├── ppg
├── repo
│ ├── __init__.py
│ ├── ubuntu.py
│ ├── fedora.py
│ └── arch.py
├── sign
│ ├── __init__.py
│ └── windows.py
├── freeze
│ ├── hooks
│ │ ├── __init__.py
│ │ ├── hook-shiboken2.py
│ │ ├── hook-shiboken6.py
│ │ ├── hook-PySide2.py
│ │ └── hook-PySide6.py
│ ├── arch.py
│ ├── fedora.py
│ ├── linux.py
│ ├── ubuntu.py
│ ├── mac.py
│ ├── __init__.py
│ └── windows.py
├── sign_installer
│ ├── __init__.py
│ ├── windows.py
│ ├── fedora.py
│ └── arch.py
├── _defaults
│ ├── requirements
│ │ ├── base.txt
│ │ ├── linux.txt
│ │ ├── ubuntu.txt
│ │ ├── fedora.txt
│ │ └── arch.txt
│ └── src
│ │ ├── build
│ │ ├── settings
│ │ │ ├── release.json
│ │ │ ├── ubuntu.json
│ │ │ ├── arch.json
│ │ │ ├── windows.json
│ │ │ ├── mac.json
│ │ │ ├── fedora.json
│ │ │ ├── linux.json
│ │ │ └── base.json
│ │ └── docker
│ │ │ ├── fedora
│ │ │ ├── .rpmmacros
│ │ │ ├── .bashrc
│ │ │ ├── gpg-agent.conf
│ │ │ ├── motd
│ │ │ └── Dockerfile
│ │ │ ├── ubuntu
│ │ │ ├── .bashrc
│ │ │ ├── gpg-agent.conf
│ │ │ ├── gpg.conf
│ │ │ ├── motd
│ │ │ └── Dockerfile
│ │ │ └── arch
│ │ │ ├── gpg-agent.conf
│ │ │ ├── .bashrc
│ │ │ ├── motd
│ │ │ └── Dockerfile
│ │ ├── repo
│ │ ├── fedora
│ │ │ └── AppName.repo
│ │ └── ubuntu
│ │ │ └── distributions
│ │ ├── installer
│ │ ├── linux
│ │ │ └── usr
│ │ │ │ └── share
│ │ │ │ └── applications
│ │ │ │ └── AppName.desktop
│ │ └── windows
│ │ │ └── Installer.nsi
│ │ └── freeze
│ │ ├── mac
│ │ └── Contents
│ │ │ └── Info.plist
│ │ └── windows
│ │ └── version_info.py
├── builtin_commands
│ ├── project_template
│ │ ├── requirements
│ │ │ ├── mac.txt
│ │ │ ├── base.txt
│ │ │ ├── linux.txt
│ │ │ └── windows.txt
│ │ └── src
│ │ │ ├── main
│ │ │ ├── resources
│ │ │ │ └── base
│ │ │ │ │ └── .gitkeep
│ │ │ ├── icons
│ │ │ │ ├── Icon.ico
│ │ │ │ ├── base
│ │ │ │ │ ├── 16.png
│ │ │ │ │ ├── 24.png
│ │ │ │ │ ├── 32.png
│ │ │ │ │ └── 64.png
│ │ │ │ ├── mac
│ │ │ │ │ ├── 1024.png
│ │ │ │ │ ├── 128.png
│ │ │ │ │ ├── 256.png
│ │ │ │ │ └── 512.png
│ │ │ │ ├── linux
│ │ │ │ │ ├── 1024.png
│ │ │ │ │ ├── 128.png
│ │ │ │ │ ├── 256.png
│ │ │ │ │ └── 512.png
│ │ │ │ └── README.md
│ │ │ └── python
│ │ │ │ └── main.py
│ │ │ └── build
│ │ │ └── settings
│ │ │ ├── mac.json
│ │ │ ├── linux.json
│ │ │ └── base.json
│ ├── _gpg
│ │ ├── gpg-agent.conf
│ │ ├── Dockerfile
│ │ ├── genkey.sh
│ │ └── __init__.py
│ ├── components.py
│ ├── _account.py
│ ├── _licensing.py
│ ├── _util.py
│ └── _docker.py
├── installer
│ ├── mac
│ │ ├── create-dmg
│ │ │ ├── examples
│ │ │ │ └── 01-main-example
│ │ │ │ │ ├── source_folder
│ │ │ │ │ └── Application.app
│ │ │ │ │ ├── installer_background.png
│ │ │ │ │ └── sample
│ │ │ ├── .gitignore
│ │ │ ├── tests
│ │ │ │ └── 007-space-in-dir-name
│ │ │ │ │ ├── my files
│ │ │ │ │ └── hello.txt
│ │ │ │ │ └── run-test
│ │ │ ├── .this-is-the-create-dmg-repo
│ │ │ ├── doc-project
│ │ │ │ ├── Release Checklist.md
│ │ │ │ └── Developer Notes.md
│ │ │ ├── .editorconfig
│ │ │ ├── builder
│ │ │ │ └── create-dmg.builder
│ │ │ ├── Makefile
│ │ │ ├── LICENSE
│ │ │ └── support
│ │ │ │ ├── template.applescript
│ │ │ │ └── eula-resources-template.xml
│ │ └── __init__.py
│ ├── fedora.py
│ ├── ubuntu.py
│ ├── __init__.py
│ ├── arch.py
│ ├── windows.py
│ └── linux.py
├── _state.py
├── _server.py
├── _aws.py
├── upload.py
├── __main__.py
├── _gpg.py
├── __init__.py
├── cmdline.py
└── resources.py
├── examples
├── pydux
│ ├── requirements
│ │ ├── base.txt
│ │ ├── linux.txt
│ │ ├── mac.txt
│ │ └── windows.txt
│ └── src
│ │ └── main
│ │ ├── resources
│ │ └── base
│ │ │ └── .gitkeep
│ │ ├── icons
│ │ ├── Icon.ico
│ │ ├── base
│ │ │ ├── 16.png
│ │ │ ├── 24.png
│ │ │ ├── 32.png
│ │ │ └── 64.png
│ │ ├── mac
│ │ │ ├── 1024.png
│ │ │ ├── 128.png
│ │ │ ├── 256.png
│ │ │ └── 512.png
│ │ ├── linux
│ │ │ ├── 1024.png
│ │ │ ├── 128.png
│ │ │ ├── 256.png
│ │ │ └── 512.png
│ │ └── README.md
│ │ └── python
│ │ └── main.py
├── first-app
│ ├── requirements
│ │ ├── base.txt
│ │ ├── linux.txt
│ │ ├── mac.txt
│ │ └── windows.txt
│ └── src
│ │ └── main
│ │ ├── resources
│ │ └── base
│ │ │ └── .gitkeep
│ │ ├── icons
│ │ ├── Icon.ico
│ │ ├── base
│ │ │ ├── 16.png
│ │ │ ├── 24.png
│ │ │ ├── 32.png
│ │ │ └── 64.png
│ │ ├── mac
│ │ │ ├── 128.png
│ │ │ ├── 256.png
│ │ │ ├── 512.png
│ │ │ └── 1024.png
│ │ ├── linux
│ │ │ ├── 128.png
│ │ │ ├── 256.png
│ │ │ ├── 512.png
│ │ │ └── 1024.png
│ │ └── README.md
│ │ └── python
│ │ └── main.py
├── hybryd-app
│ ├── requirements
│ │ ├── base.txt
│ │ ├── linux.txt
│ │ ├── mac.txt
│ │ └── windows.txt
│ └── src
│ │ └── main
│ │ ├── resources
│ │ └── base
│ │ │ ├── .gitkeep
│ │ │ └── index.html
│ │ ├── icons
│ │ ├── Icon.ico
│ │ ├── base
│ │ │ ├── 16.png
│ │ │ ├── 24.png
│ │ │ ├── 32.png
│ │ │ └── 64.png
│ │ ├── mac
│ │ │ ├── 1024.png
│ │ │ ├── 128.png
│ │ │ ├── 256.png
│ │ │ └── 512.png
│ │ ├── linux
│ │ │ ├── 1024.png
│ │ │ ├── 128.png
│ │ │ ├── 256.png
│ │ │ └── 512.png
│ │ └── README.md
│ │ └── python
│ │ └── main.py
├── multi-screens
│ ├── requirements
│ │ ├── mac.txt
│ │ ├── base.txt
│ │ ├── linux.txt
│ │ └── windows.txt
│ └── src
│ │ └── main
│ │ ├── resources
│ │ └── base
│ │ │ └── .gitkeep
│ │ ├── icons
│ │ ├── Icon.ico
│ │ ├── base
│ │ │ ├── 16.png
│ │ │ ├── 24.png
│ │ │ ├── 32.png
│ │ │ └── 64.png
│ │ ├── mac
│ │ │ ├── 1024.png
│ │ │ ├── 128.png
│ │ │ ├── 256.png
│ │ │ └── 512.png
│ │ ├── linux
│ │ │ ├── 1024.png
│ │ │ ├── 128.png
│ │ │ ├── 256.png
│ │ │ └── 512.png
│ │ └── README.md
│ │ └── python
│ │ ├── views
│ │ ├── Home.py
│ │ └── AuthScreen.py
│ │ └── main.py
└── reactive-components
│ ├── requirements
│ ├── base.txt
│ ├── linux.txt
│ ├── mac.txt
│ └── windows.txt
│ └── src
│ └── main
│ ├── resources
│ └── base
│ │ └── .gitkeep
│ ├── icons
│ ├── Icon.ico
│ ├── base
│ │ ├── 16.png
│ │ ├── 24.png
│ │ ├── 32.png
│ │ └── 64.png
│ ├── mac
│ │ ├── 128.png
│ │ ├── 256.png
│ │ ├── 512.png
│ │ └── 1024.png
│ ├── linux
│ │ ├── 128.png
│ │ ├── 256.png
│ │ ├── 512.png
│ │ └── 1024.png
│ └── README.md
│ └── python
│ └── main.py
├── ppg_runtime
├── application_context
│ ├── devtools
│ │ └── __init__.py
│ ├── utils
│ │ └── __init__.py
│ ├── PyQt6.py
│ ├── PyQt5.py
│ ├── PySide2.py
│ └── PySide6.py
├── __init__.py
├── _resources.py
├── _frozen.py
├── excepthook
│ ├── _util.py
│ └── sentry.py
├── _state.py
├── ReactiveWidgets
│ ├── __init__.py
│ ├── label.py
│ ├── check_box.py
│ ├── text_edit.py
│ ├── line_edit.py
│ ├── spin_box.py
│ ├── slider.py
│ └── combo_box.py
├── _fbs.py
├── _settings.py
├── _source.py
├── _signal.py
├── platform.py
└── licensing.py
├── setup.cfg
├── .gitattributes
├── requirements.txt
├── MANIFEST.in
├── .gitignore
├── package.json
└── setup.py
/ppg/repo/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/ppg/sign/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/ppg/freeze/hooks/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/ppg/sign_installer/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/pydux/requirements/base.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/pydux/requirements/linux.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/pydux/requirements/mac.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/first-app/requirements/base.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/first-app/requirements/linux.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/first-app/requirements/mac.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/hybryd-app/requirements/base.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/hybryd-app/requirements/linux.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/hybryd-app/requirements/mac.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/multi-screens/requirements/mac.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/first-app/requirements/windows.txt:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/examples/multi-screens/requirements/base.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/multi-screens/requirements/linux.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/pydux/requirements/windows.txt:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/examples/pydux/src/main/resources/base/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/ppg/_defaults/requirements/base.txt:
--------------------------------------------------------------------------------
1 | ppg==1.0.2
--------------------------------------------------------------------------------
/ppg/_defaults/requirements/linux.txt:
--------------------------------------------------------------------------------
1 | -r base.txt
--------------------------------------------------------------------------------
/examples/first-app/src/main/resources/base/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/hybryd-app/requirements/windows.txt:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/examples/hybryd-app/src/main/resources/base/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/multi-screens/requirements/windows.txt:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/examples/reactive-components/requirements/base.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/reactive-components/requirements/linux.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/reactive-components/requirements/mac.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/ppg_runtime/application_context/devtools/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | description-file = README.md
--------------------------------------------------------------------------------
/examples/multi-screens/src/main/resources/base/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/reactive-components/requirements/windows.txt:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/ppg/builtin_commands/project_template/requirements/mac.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/reactive-components/src/main/resources/base/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/ppg/builtin_commands/project_template/requirements/base.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/ppg/builtin_commands/project_template/requirements/linux.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/ppg_runtime/__init__.py:
--------------------------------------------------------------------------------
1 | class FbsError(Exception):
2 | pass
--------------------------------------------------------------------------------
/ppg/_defaults/requirements/ubuntu.txt:
--------------------------------------------------------------------------------
1 | -r linux.txt
2 | PyQt5==5.9.2
--------------------------------------------------------------------------------
/ppg/builtin_commands/project_template/requirements/windows.txt:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/ppg/builtin_commands/project_template/src/main/resources/base/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/ppg/installer/mac/create-dmg/examples/01-main-example/source_folder/Application.app:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/ppg/installer/mac/create-dmg/.gitignore:
--------------------------------------------------------------------------------
1 | .svn
2 | .vscode
3 |
4 | *.dmg
5 | .DS_Store
6 |
--------------------------------------------------------------------------------
/ppg/installer/mac/create-dmg/tests/007-space-in-dir-name/my files/hello.txt:
--------------------------------------------------------------------------------
1 | Hello world
2 |
--------------------------------------------------------------------------------
/ppg/_defaults/src/build/settings/release.json:
--------------------------------------------------------------------------------
1 | {
2 | "release": true,
3 | "environment": "production"
4 | }
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | PyInstaller==6.9.0
2 | rsa>=3.4.2
3 | boto3
4 | sentry-sdk>=0.6.6
5 | termcolor==1.1.0
6 |
7 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | recursive-include ppg/builtin_commands/project_template *
2 | include ppg/builtin_commands/package.json
--------------------------------------------------------------------------------
/examples/pydux/src/main/icons/Icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/pydux/src/main/icons/Icon.ico
--------------------------------------------------------------------------------
/ppg/freeze/arch.py:
--------------------------------------------------------------------------------
1 | from ppg.freeze.linux import freeze_linux
2 |
3 | def freeze_arch(debug=False):
4 | freeze_linux(debug)
--------------------------------------------------------------------------------
/examples/first-app/src/main/icons/Icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/first-app/src/main/icons/Icon.ico
--------------------------------------------------------------------------------
/examples/pydux/src/main/icons/base/16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/pydux/src/main/icons/base/16.png
--------------------------------------------------------------------------------
/examples/pydux/src/main/icons/base/24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/pydux/src/main/icons/base/24.png
--------------------------------------------------------------------------------
/examples/pydux/src/main/icons/base/32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/pydux/src/main/icons/base/32.png
--------------------------------------------------------------------------------
/examples/pydux/src/main/icons/base/64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/pydux/src/main/icons/base/64.png
--------------------------------------------------------------------------------
/examples/pydux/src/main/icons/mac/1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/pydux/src/main/icons/mac/1024.png
--------------------------------------------------------------------------------
/examples/pydux/src/main/icons/mac/128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/pydux/src/main/icons/mac/128.png
--------------------------------------------------------------------------------
/examples/pydux/src/main/icons/mac/256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/pydux/src/main/icons/mac/256.png
--------------------------------------------------------------------------------
/examples/pydux/src/main/icons/mac/512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/pydux/src/main/icons/mac/512.png
--------------------------------------------------------------------------------
/ppg/builtin_commands/project_template/src/build/settings/mac.json:
--------------------------------------------------------------------------------
1 | {
2 | "mac_bundle_identifier": "${mac_bundle_identifier}"
3 | }
--------------------------------------------------------------------------------
/examples/first-app/src/main/icons/base/16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/first-app/src/main/icons/base/16.png
--------------------------------------------------------------------------------
/examples/first-app/src/main/icons/base/24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/first-app/src/main/icons/base/24.png
--------------------------------------------------------------------------------
/examples/first-app/src/main/icons/base/32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/first-app/src/main/icons/base/32.png
--------------------------------------------------------------------------------
/examples/first-app/src/main/icons/base/64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/first-app/src/main/icons/base/64.png
--------------------------------------------------------------------------------
/examples/first-app/src/main/icons/mac/128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/first-app/src/main/icons/mac/128.png
--------------------------------------------------------------------------------
/examples/first-app/src/main/icons/mac/256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/first-app/src/main/icons/mac/256.png
--------------------------------------------------------------------------------
/examples/first-app/src/main/icons/mac/512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/first-app/src/main/icons/mac/512.png
--------------------------------------------------------------------------------
/examples/hybryd-app/src/main/icons/Icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/hybryd-app/src/main/icons/Icon.ico
--------------------------------------------------------------------------------
/examples/pydux/src/main/icons/linux/1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/pydux/src/main/icons/linux/1024.png
--------------------------------------------------------------------------------
/examples/pydux/src/main/icons/linux/128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/pydux/src/main/icons/linux/128.png
--------------------------------------------------------------------------------
/examples/pydux/src/main/icons/linux/256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/pydux/src/main/icons/linux/256.png
--------------------------------------------------------------------------------
/examples/pydux/src/main/icons/linux/512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/pydux/src/main/icons/linux/512.png
--------------------------------------------------------------------------------
/examples/first-app/src/main/icons/linux/128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/first-app/src/main/icons/linux/128.png
--------------------------------------------------------------------------------
/examples/first-app/src/main/icons/linux/256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/first-app/src/main/icons/linux/256.png
--------------------------------------------------------------------------------
/examples/first-app/src/main/icons/linux/512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/first-app/src/main/icons/linux/512.png
--------------------------------------------------------------------------------
/examples/first-app/src/main/icons/mac/1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/first-app/src/main/icons/mac/1024.png
--------------------------------------------------------------------------------
/examples/hybryd-app/src/main/icons/base/16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/hybryd-app/src/main/icons/base/16.png
--------------------------------------------------------------------------------
/examples/hybryd-app/src/main/icons/base/24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/hybryd-app/src/main/icons/base/24.png
--------------------------------------------------------------------------------
/examples/hybryd-app/src/main/icons/base/32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/hybryd-app/src/main/icons/base/32.png
--------------------------------------------------------------------------------
/examples/hybryd-app/src/main/icons/base/64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/hybryd-app/src/main/icons/base/64.png
--------------------------------------------------------------------------------
/examples/hybryd-app/src/main/icons/mac/1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/hybryd-app/src/main/icons/mac/1024.png
--------------------------------------------------------------------------------
/examples/hybryd-app/src/main/icons/mac/128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/hybryd-app/src/main/icons/mac/128.png
--------------------------------------------------------------------------------
/examples/hybryd-app/src/main/icons/mac/256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/hybryd-app/src/main/icons/mac/256.png
--------------------------------------------------------------------------------
/examples/hybryd-app/src/main/icons/mac/512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/hybryd-app/src/main/icons/mac/512.png
--------------------------------------------------------------------------------
/examples/multi-screens/src/main/icons/Icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/multi-screens/src/main/icons/Icon.ico
--------------------------------------------------------------------------------
/examples/first-app/src/main/icons/linux/1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/first-app/src/main/icons/linux/1024.png
--------------------------------------------------------------------------------
/examples/hybryd-app/src/main/icons/linux/1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/hybryd-app/src/main/icons/linux/1024.png
--------------------------------------------------------------------------------
/examples/hybryd-app/src/main/icons/linux/128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/hybryd-app/src/main/icons/linux/128.png
--------------------------------------------------------------------------------
/examples/hybryd-app/src/main/icons/linux/256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/hybryd-app/src/main/icons/linux/256.png
--------------------------------------------------------------------------------
/examples/hybryd-app/src/main/icons/linux/512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/hybryd-app/src/main/icons/linux/512.png
--------------------------------------------------------------------------------
/examples/multi-screens/src/main/icons/base/16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/multi-screens/src/main/icons/base/16.png
--------------------------------------------------------------------------------
/examples/multi-screens/src/main/icons/base/24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/multi-screens/src/main/icons/base/24.png
--------------------------------------------------------------------------------
/examples/multi-screens/src/main/icons/base/32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/multi-screens/src/main/icons/base/32.png
--------------------------------------------------------------------------------
/examples/multi-screens/src/main/icons/base/64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/multi-screens/src/main/icons/base/64.png
--------------------------------------------------------------------------------
/examples/multi-screens/src/main/icons/mac/1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/multi-screens/src/main/icons/mac/1024.png
--------------------------------------------------------------------------------
/examples/multi-screens/src/main/icons/mac/128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/multi-screens/src/main/icons/mac/128.png
--------------------------------------------------------------------------------
/examples/multi-screens/src/main/icons/mac/256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/multi-screens/src/main/icons/mac/256.png
--------------------------------------------------------------------------------
/examples/multi-screens/src/main/icons/mac/512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/multi-screens/src/main/icons/mac/512.png
--------------------------------------------------------------------------------
/ppg/_defaults/src/build/docker/fedora/.rpmmacros:
--------------------------------------------------------------------------------
1 | %_signature gpg
2 | %_gpg_path /root/.gnupg
3 | %_gpg_name ${gpg_name}
4 | %_gpgbin /usr/bin/gpg
--------------------------------------------------------------------------------
/examples/multi-screens/src/main/icons/linux/1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/multi-screens/src/main/icons/linux/1024.png
--------------------------------------------------------------------------------
/examples/multi-screens/src/main/icons/linux/128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/multi-screens/src/main/icons/linux/128.png
--------------------------------------------------------------------------------
/examples/multi-screens/src/main/icons/linux/256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/multi-screens/src/main/icons/linux/256.png
--------------------------------------------------------------------------------
/examples/multi-screens/src/main/icons/linux/512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/multi-screens/src/main/icons/linux/512.png
--------------------------------------------------------------------------------
/examples/reactive-components/src/main/icons/Icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/reactive-components/src/main/icons/Icon.ico
--------------------------------------------------------------------------------
/ppg/_defaults/src/build/docker/fedora/.bashrc:
--------------------------------------------------------------------------------
1 | PS1='fedora:\W$ '
2 |
3 | # Display welcome message:
4 | [ ! -z "$TERM" -a -r /etc/motd ] && cat /etc/motd
--------------------------------------------------------------------------------
/ppg/_defaults/src/build/docker/ubuntu/.bashrc:
--------------------------------------------------------------------------------
1 | PS1='ubuntu:\W$ '
2 |
3 | # Display welcome message:
4 | [ ! -z "$TERM" -a -r /etc/motd ] && cat /etc/motd
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__/
2 | build/
3 | dist/
4 | ppg.egg-info/
5 | .vscode/
6 | target/
7 | .DS_Store
8 | test/
9 | tests/
10 | test.py
11 | *.pyc
--------------------------------------------------------------------------------
/examples/reactive-components/src/main/icons/base/16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/reactive-components/src/main/icons/base/16.png
--------------------------------------------------------------------------------
/examples/reactive-components/src/main/icons/base/24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/reactive-components/src/main/icons/base/24.png
--------------------------------------------------------------------------------
/examples/reactive-components/src/main/icons/base/32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/reactive-components/src/main/icons/base/32.png
--------------------------------------------------------------------------------
/examples/reactive-components/src/main/icons/base/64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/reactive-components/src/main/icons/base/64.png
--------------------------------------------------------------------------------
/examples/reactive-components/src/main/icons/mac/128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/reactive-components/src/main/icons/mac/128.png
--------------------------------------------------------------------------------
/examples/reactive-components/src/main/icons/mac/256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/reactive-components/src/main/icons/mac/256.png
--------------------------------------------------------------------------------
/examples/reactive-components/src/main/icons/mac/512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/reactive-components/src/main/icons/mac/512.png
--------------------------------------------------------------------------------
/ppg/_defaults/requirements/fedora.txt:
--------------------------------------------------------------------------------
1 | # Because Fedora is also based on Gnome, we can typically use the same
2 | # requirements as Ubuntu:
3 | -r ubuntu.txt
--------------------------------------------------------------------------------
/ppg/_defaults/src/build/docker/arch/gpg-agent.conf:
--------------------------------------------------------------------------------
1 | # Allows us to preload the GPG pass phrase for a key via gpg-preset-passphrase:
2 | allow-preset-passphrase
--------------------------------------------------------------------------------
/ppg/_defaults/src/repo/fedora/AppName.repo:
--------------------------------------------------------------------------------
1 | [${app_name}]
2 | name=${app_name}
3 | baseurl=${repo_url}
4 | enabled=1
5 | gpgcheck=1
6 | gpgkey=${gpgkey_url}
--------------------------------------------------------------------------------
/examples/reactive-components/src/main/icons/linux/128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/reactive-components/src/main/icons/linux/128.png
--------------------------------------------------------------------------------
/examples/reactive-components/src/main/icons/linux/256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/reactive-components/src/main/icons/linux/256.png
--------------------------------------------------------------------------------
/examples/reactive-components/src/main/icons/linux/512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/reactive-components/src/main/icons/linux/512.png
--------------------------------------------------------------------------------
/examples/reactive-components/src/main/icons/mac/1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/reactive-components/src/main/icons/mac/1024.png
--------------------------------------------------------------------------------
/ppg/_defaults/src/build/docker/fedora/gpg-agent.conf:
--------------------------------------------------------------------------------
1 | # Allows us to preload the GPG pass phrase for a key via gpg-preset-passphrase:
2 | allow-preset-passphrase
--------------------------------------------------------------------------------
/ppg/_defaults/src/build/docker/ubuntu/gpg-agent.conf:
--------------------------------------------------------------------------------
1 | # Allows us to preload the GPG pass phrase for a key via gpg-preset-passphrase:
2 | allow-preset-passphrase
--------------------------------------------------------------------------------
/ppg_runtime/application_context/utils/__init__.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 |
4 | def app_is_frozen(): return getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS')
--------------------------------------------------------------------------------
/examples/reactive-components/src/main/icons/linux/1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/examples/reactive-components/src/main/icons/linux/1024.png
--------------------------------------------------------------------------------
/ppg/builtin_commands/project_template/src/main/icons/Icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/ppg/builtin_commands/project_template/src/main/icons/Icon.ico
--------------------------------------------------------------------------------
/ppg/builtin_commands/project_template/src/main/icons/base/16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/ppg/builtin_commands/project_template/src/main/icons/base/16.png
--------------------------------------------------------------------------------
/ppg/builtin_commands/project_template/src/main/icons/base/24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/ppg/builtin_commands/project_template/src/main/icons/base/24.png
--------------------------------------------------------------------------------
/ppg/builtin_commands/project_template/src/main/icons/base/32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/ppg/builtin_commands/project_template/src/main/icons/base/32.png
--------------------------------------------------------------------------------
/ppg/builtin_commands/project_template/src/main/icons/base/64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/ppg/builtin_commands/project_template/src/main/icons/base/64.png
--------------------------------------------------------------------------------
/ppg/builtin_commands/project_template/src/main/icons/mac/1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/ppg/builtin_commands/project_template/src/main/icons/mac/1024.png
--------------------------------------------------------------------------------
/ppg/builtin_commands/project_template/src/main/icons/mac/128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/ppg/builtin_commands/project_template/src/main/icons/mac/128.png
--------------------------------------------------------------------------------
/ppg/builtin_commands/project_template/src/main/icons/mac/256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/ppg/builtin_commands/project_template/src/main/icons/mac/256.png
--------------------------------------------------------------------------------
/ppg/builtin_commands/project_template/src/main/icons/mac/512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/ppg/builtin_commands/project_template/src/main/icons/mac/512.png
--------------------------------------------------------------------------------
/ppg/_defaults/requirements/arch.txt:
--------------------------------------------------------------------------------
1 | -r linux.txt
2 | # PyQt5==5.11 currently gives an error when we deploy and run the frozen app:
3 | # No module named 'PyQt5.sip'
4 | PyQt5<5.11
--------------------------------------------------------------------------------
/ppg/builtin_commands/project_template/src/build/settings/linux.json:
--------------------------------------------------------------------------------
1 | {
2 | "categories": "Utility;",
3 | "description": "",
4 | "author_email": "",
5 | "url": ""
6 | }
--------------------------------------------------------------------------------
/ppg/builtin_commands/project_template/src/main/icons/linux/1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/ppg/builtin_commands/project_template/src/main/icons/linux/1024.png
--------------------------------------------------------------------------------
/ppg/builtin_commands/project_template/src/main/icons/linux/128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/ppg/builtin_commands/project_template/src/main/icons/linux/128.png
--------------------------------------------------------------------------------
/ppg/builtin_commands/project_template/src/main/icons/linux/256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/ppg/builtin_commands/project_template/src/main/icons/linux/256.png
--------------------------------------------------------------------------------
/ppg/builtin_commands/project_template/src/main/icons/linux/512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/ppg/builtin_commands/project_template/src/main/icons/linux/512.png
--------------------------------------------------------------------------------
/ppg/installer/fedora.py:
--------------------------------------------------------------------------------
1 | from ppg.installer.linux import generate_installer_files, run_fpm
2 |
3 | def create_installer_fedora():
4 | generate_installer_files()
5 | run_fpm('rpm')
--------------------------------------------------------------------------------
/ppg/installer/ubuntu.py:
--------------------------------------------------------------------------------
1 | from ppg.installer.linux import generate_installer_files, run_fpm
2 |
3 | def create_installer_ubuntu():
4 | generate_installer_files()
5 | run_fpm('deb')
--------------------------------------------------------------------------------
/ppg/builtin_commands/_gpg/gpg-agent.conf:
--------------------------------------------------------------------------------
1 | # Let us provide the GPG passphrase via a pipe | when exporting the private key:
2 | pinentry-program /usr/bin/pinentry-tty
3 | allow-loopback-pinentry
--------------------------------------------------------------------------------
/ppg/installer/mac/create-dmg/.this-is-the-create-dmg-repo:
--------------------------------------------------------------------------------
1 | This is just a dummy file so create-dmg can tell whether it's being run from
2 | inside the Git repo or from an installed location.
3 |
--------------------------------------------------------------------------------
/ppg/_defaults/src/build/settings/ubuntu.json:
--------------------------------------------------------------------------------
1 | {
2 | "installer": "${app_name}.deb",
3 | "gpg_preset_passphrase": "/usr/lib/gnupg2/gpg-preset-passphrase",
4 | "repo_subdir": "deb"
5 | }
--------------------------------------------------------------------------------
/ppg/installer/mac/create-dmg/examples/01-main-example/installer_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neuri-AI/PPG/HEAD/ppg/installer/mac/create-dmg/examples/01-main-example/installer_background.png
--------------------------------------------------------------------------------
/ppg/_defaults/src/repo/ubuntu/distributions:
--------------------------------------------------------------------------------
1 | Origin: ${app_name}
2 | Label: ${app_name}
3 | Codename: stable
4 | Architectures: amd64
5 | Components: main
6 | Description: ${description}
7 | SignWith: ${gpg_key}
8 |
--------------------------------------------------------------------------------
/ppg/builtin_commands/project_template/src/build/settings/base.json:
--------------------------------------------------------------------------------
1 | {
2 | "app_name": "${app_name}",
3 | "author": "${author}",
4 | "main_module": "src/main/python/main.py",
5 | "version": "0.0.0"
6 | }
--------------------------------------------------------------------------------
/ppg/_defaults/src/build/docker/arch/.bashrc:
--------------------------------------------------------------------------------
1 | # Place fpm on the PATH:
2 | export PATH=$PATH:$(ruby -e "puts Gem.user_dir")/bin
3 | PS1='arch:\W$ '
4 |
5 | # Display welcome message:
6 | [ ! -z "$TERM" -a -r /etc/motd ] && cat /etc/motd
--------------------------------------------------------------------------------
/ppg/installer/mac/create-dmg/tests/007-space-in-dir-name/run-test:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Test for https://github.com/create-dmg/create-dmg/issues/7 - spaces in folder names
4 |
5 | ../../create-dmg "my disk image.dmg" "my files"
--------------------------------------------------------------------------------
/ppg/_defaults/src/build/settings/arch.json:
--------------------------------------------------------------------------------
1 | {
2 | "installer": "${app_name}.pkg.tar.xz",
3 | "depends": ["qt5-base"],
4 | "depends_opt": [],
5 | "gpg_preset_passphrase": "/usr/lib/gnupg/gpg-preset-passphrase",
6 | "repo_subdir": "arch"
7 | }
--------------------------------------------------------------------------------
/ppg/sign_installer/windows.py:
--------------------------------------------------------------------------------
1 | from ppg import path, SETTINGS
2 | from ppg.sign.windows import sign_file
3 |
4 | def sign_installer_windows():
5 | installer = path('target/${installer}')
6 | sign_file(installer, SETTINGS['app_name'] + ' Setup', SETTINGS['url'])
--------------------------------------------------------------------------------
/ppg/_defaults/src/installer/linux/usr/share/applications/AppName.desktop:
--------------------------------------------------------------------------------
1 | [Desktop Entry]
2 | Name=${app_name}
3 | Type=Application
4 | Exec=/opt/${app_name}/${app_name}
5 | Terminal=false
6 | NoDisplay=false
7 | Categories=${categories}
8 | Version=1.2
9 | Icon=${app_name}
--------------------------------------------------------------------------------
/ppg/_defaults/src/build/settings/windows.json:
--------------------------------------------------------------------------------
1 | {
2 | "installer": "${app_name}Setup.exe",
3 | "files_to_filter": [
4 | "src/freeze/windows/version_info.py",
5 | "src/installer/windows/Installer.nsi"
6 | ],
7 | "show_console_window": false,
8 | "url": ""
9 | }
--------------------------------------------------------------------------------
/ppg/_defaults/src/build/settings/mac.json:
--------------------------------------------------------------------------------
1 | {
2 | "freeze_dir": "target/${app_name}.app",
3 | "mac_bundle_identifier": "",
4 | "installer": "${app_name}.dmg",
5 | "files_to_filter": [
6 | "src/freeze/mac/Contents/Info.plist"
7 | ],
8 | "show_console_window": false
9 | }
--------------------------------------------------------------------------------
/ppg/_defaults/src/build/settings/fedora.json:
--------------------------------------------------------------------------------
1 | {
2 | "installer": "${app_name}.rpm",
3 | "gpg_preset_passphrase": "/usr/libexec/gpg-preset-passphrase",
4 | "repo_url": "https://fbs.sh/${fbs_user}/${app_name}/${repo_subdir}",
5 | "gpgkey_url": "https://fbs.sh/${fbs_user}/${app_name}/public-key.gpg",
6 | "repo_subdir": "rpm"
7 | }
--------------------------------------------------------------------------------
/examples/pydux/src/main/icons/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | You can create Icon.ico from the .png files with
4 | [an online tool](http://icoconvert.com/Multi_Image_to_one_icon/).
5 |
--------------------------------------------------------------------------------
/ppg/installer/__init__.py:
--------------------------------------------------------------------------------
1 | from ppg import path, LOADED_PROFILES
2 | from ppg.resources import _copy
3 | from ppg_runtime._source import default_path
4 |
5 | def _generate_installer_resources():
6 | for path_fn in default_path, path:
7 | for profile in LOADED_PROFILES:
8 | _copy(path_fn, 'src/installer/' + profile, path('target/installer'))
--------------------------------------------------------------------------------
/examples/first-app/src/main/icons/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | You can create Icon.ico from the .png files with
4 | [an online tool](http://icoconvert.com/Multi_Image_to_one_icon/).
5 |
--------------------------------------------------------------------------------
/examples/hybryd-app/src/main/icons/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | You can create Icon.ico from the .png files with
4 | [an online tool](http://icoconvert.com/Multi_Image_to_one_icon/).
5 |
--------------------------------------------------------------------------------
/examples/multi-screens/src/main/icons/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | You can create Icon.ico from the .png files with
4 | [an online tool](http://icoconvert.com/Multi_Image_to_one_icon/).
5 |
--------------------------------------------------------------------------------
/examples/reactive-components/src/main/icons/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | You can create Icon.ico from the .png files with
4 | [an online tool](http://icoconvert.com/Multi_Image_to_one_icon/).
5 |
--------------------------------------------------------------------------------
/ppg/_defaults/src/build/settings/linux.json:
--------------------------------------------------------------------------------
1 | {
2 | "categories": "Utility;",
3 | "description": "",
4 | "author_email": "",
5 | "url": "",
6 | "depends": [],
7 | "files_to_filter": [
8 | "src/installer/linux/usr/share/applications/AppName.desktop"
9 | ],
10 | "public_settings": ["categories", "description", "author_email", "url"]
11 | }
--------------------------------------------------------------------------------
/ppg/builtin_commands/project_template/src/main/icons/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | You can create Icon.ico from the .png files with
4 | [an online tool](http://icoconvert.com/Multi_Image_to_one_icon/).
5 |
--------------------------------------------------------------------------------
/ppg/builtin_commands/_gpg/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM debian:9.6
2 |
3 | ARG name
4 | ARG email
5 | ARG passphrase
6 | ARG keylength=1024
7 |
8 | RUN apt-get update
9 |
10 | ADD gpg-agent.conf /root/.gnupg/gpg-agent.conf
11 | RUN chmod -R 600 /root/.gnupg/
12 | RUN apt-get install gnupg2 pinentry-tty -y
13 |
14 | WORKDIR /root
15 |
16 | ADD genkey.sh /root/genkey.sh
17 | RUN chmod +x genkey.sh
--------------------------------------------------------------------------------
/ppg/installer/arch.py:
--------------------------------------------------------------------------------
1 | from ppg import path
2 | from ppg.installer.linux import generate_installer_files, run_fpm
3 | from subprocess import run
4 |
5 | def create_installer_arch():
6 | generate_installer_files()
7 | # Avoid pacman warning "directory permissions differ" when installing:
8 | run(['chmod', 'g-w', '-R', path('target/installer')], check=True)
9 | run_fpm('pacman')
--------------------------------------------------------------------------------
/ppg/sign_installer/fedora.py:
--------------------------------------------------------------------------------
1 | from ppg import path
2 | from ppg._gpg import preset_gpg_passphrase
3 | from subprocess import check_call, DEVNULL
4 |
5 | def sign_installer_fedora():
6 | # Prevent GPG from prompting us for the passphrase when signing:
7 | preset_gpg_passphrase()
8 | check_call(
9 | ['rpm', '--addsign', path('target/${installer}')], stdout=DEVNULL
10 | )
--------------------------------------------------------------------------------
/examples/multi-screens/src/main/python/views/Home.py:
--------------------------------------------------------------------------------
1 |
2 | from ppg_runtime.application_context import Pydux, PPGLifeCycle, init_lifecycle
3 | from PySide6.QtWidgets import QWidget, QLabel
4 |
5 | @init_lifecycle
6 | class Home(QWidget, PPGLifeCycle, Pydux):
7 |
8 | def component_will_mount(self):
9 | self.subscribe_to_store(self)
10 |
11 | def render_(self):
12 | QLabel('Welcome Home!', self)
13 |
14 |
--------------------------------------------------------------------------------
/ppg/_defaults/src/build/docker/ubuntu/gpg.conf:
--------------------------------------------------------------------------------
1 | # Old GPG versions (including the one we use, on Ubuntu 14) use SHA-1 by
2 | # default. Recent Ubuntu versions (in particular, >= 18) no longer allow this
3 | # and give "The following signatures were invalid" when `apt-get update` fetches
4 | # our SHA-1 signed reprepro repository. The following setting ensures that our
5 | # reprepro uses SHA-256 instead:
6 | digest-algo sha256
--------------------------------------------------------------------------------
/ppg/builtin_commands/components.py:
--------------------------------------------------------------------------------
1 | component_template = f"""
2 | from ppg_runtime.application_context import Pydux, PPGLifeCycle, init_lifecycle
3 | from $Binding.QtWidgets import $Widget
4 |
5 | @init_lifecycle
6 | class $Name($Widget, PPGLifeCycle, Pydux):
7 |
8 | def component_will_mount(self):
9 | self.subscribe_to_store(self)
10 |
11 | def render_(self):
12 | # Render the UI here
13 | pass
14 |
15 | """
16 |
--------------------------------------------------------------------------------
/ppg/installer/mac/create-dmg/doc-project/Release Checklist.md:
--------------------------------------------------------------------------------
1 | # Release Checklist
2 |
3 | - Update the version in `create-dmg`'s `pure_version` function
4 | - Remove the "-SNAPSHOT" suffix
5 | - Commit
6 | - Tag the release as `vX.X.X`
7 | - `git push --tags`
8 | - Create a release on the GitHub project page
9 | - Open development on the next release
10 | - Bump the version number and add a "-SNAPSHOT" suffix to it
11 |
--------------------------------------------------------------------------------
/ppg/_defaults/src/build/docker/arch/motd:
--------------------------------------------------------------------------------
1 | You are now in a Docker container running Arch. To build your app
2 | for this platform, use the normal commands `fbs freeze` etc.
3 |
4 | Note that you can't launch GUIs here. So eg. `fbs run` won't work.
5 |
6 | Another caveat is that target/ here is special: It symlinks to your
7 | usual target/arch/. So when you are done and type `exit` to leave
8 | this container, you can find the produced binaries there.
9 |
--------------------------------------------------------------------------------
/ppg/_defaults/src/build/docker/fedora/motd:
--------------------------------------------------------------------------------
1 | You are now in a Docker container running Fedora. To build your app
2 | for this platform, use the normal commands `fbs freeze` etc.
3 |
4 | Note that you can't launch GUIs here. So eg. `fbs run` won't work.
5 |
6 | Another caveat is that target/ here is special: It symlinks to your
7 | usual target/fedora/. So when you are done and type `exit` to leave
8 | this container, you can find the produced binaries there.
9 |
--------------------------------------------------------------------------------
/ppg/_defaults/src/build/docker/ubuntu/motd:
--------------------------------------------------------------------------------
1 | You are now in a Docker container running Ubuntu. To build your app
2 | for this platform, use the normal commands `fbs freeze` etc.
3 |
4 | Note that you can't launch GUIs here. So eg. `fbs run` won't work.
5 |
6 | Another caveat is that target/ here is special: It symlinks to your
7 | usual target/ubuntu/. So when you are done and type `exit` to leave
8 | this container, you can find the produced binaries there.
9 |
--------------------------------------------------------------------------------
/ppg/installer/mac/create-dmg/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig for create-dmg project
2 | # EditorConfig is awesome: https://EditorConfig.org
3 |
4 | root = true
5 |
6 | [*]
7 | end_of_line = lf
8 | insert_final_newline = true
9 | charset = utf-8
10 |
11 | # We use tabs in our own code
12 | [{create-dmg,*.applescript,*.sh}]
13 | indent_style = tab
14 | indent_size = 2
15 |
16 | # But the Python code we pull in from pyhacker uses spaces
17 | [*.py]
18 | indent_style = space
19 | indent_size = 4
20 |
--------------------------------------------------------------------------------
/ppg/sign_installer/arch.py:
--------------------------------------------------------------------------------
1 | from ppg import path, SETTINGS
2 | from ppg._gpg import preset_gpg_passphrase
3 | from subprocess import check_call, DEVNULL
4 |
5 | def sign_installer_arch():
6 | installer = path('target/${installer}')
7 | # Prevent GPG from prompting us for the passphrase when signing:
8 | preset_gpg_passphrase()
9 | check_call(
10 | ['gpg', '--batch', '--yes', '-u', SETTINGS['gpg_key'],
11 | '--output', installer + '.sig', '--detach-sig', installer],
12 | stdout=DEVNULL
13 | )
--------------------------------------------------------------------------------
/ppg/builtin_commands/_gpg/genkey.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -e
4 |
5 | tmpfile=$(mktemp)
6 |
7 | cat >"$tmpfile" <]/create-dmg [[]]
16 |
17 |
18 | ZIP [create-dmg.zip]
19 | INTO [build-files-prefix] [build.dir]
20 |
21 |
22 | PUT megabox-builds create-dmg.zip
23 | PUT megabox-builds build.log
24 |
25 | PUT s3-builds create-dmg.zip
26 | PUT s3-builds build.log
27 |
--------------------------------------------------------------------------------
/ppg_runtime/excepthook/_util.py:
--------------------------------------------------------------------------------
1 | from time import time
2 |
3 | class RateLimiter:
4 | def __init__(self, interval_secs, allowance, time_fn=time):
5 | self._interval = interval_secs
6 | self._allowance = allowance
7 | self._time_fn = time_fn
8 | self._interval_start = time_fn()
9 | self._num_requests = 0
10 | def please(self):
11 | now = self._time_fn()
12 | if now > self._interval_start + self._interval:
13 | self._num_requests = 0
14 | self._interval_start = now
15 | if self._num_requests < self._allowance:
16 | self._num_requests += 1
17 | return True
18 | return False
--------------------------------------------------------------------------------
/ppg_runtime/_state.py:
--------------------------------------------------------------------------------
1 | """
2 | This INTERNAL module is used to manage fbs_runtime's global state. Having it
3 | here, in one central place, allows ppg's test suite to manipulate the state to
4 | simulate various scenarios such as different operating systems.
5 | """
6 |
7 | PLATFORM_NAME = None
8 | LINUX_DISTRIBUTION = None
9 | APPLICATION_CONTEXT = None
10 |
11 | def get():
12 | return PLATFORM_NAME, LINUX_DISTRIBUTION, APPLICATION_CONTEXT
13 |
14 | def restore(platform_name, linux_distribution, application_context):
15 | global PLATFORM_NAME, LINUX_DISTRIBUTION, APPLICATION_CONTEXT
16 | PLATFORM_NAME = platform_name
17 | LINUX_DISTRIBUTION = linux_distribution
18 | APPLICATION_CONTEXT = application_context
--------------------------------------------------------------------------------
/ppg/freeze/fedora.py:
--------------------------------------------------------------------------------
1 | from ppg.freeze.linux import freeze_linux, remove_shared_libraries
2 |
3 | def freeze_fedora(debug=False):
4 | freeze_linux(debug)
5 | # Force Fedora to use the system's Gnome libraries. This avoids warnings
6 | # when starting the app on the command line.
7 | remove_shared_libraries('libgio-2.0.so.*', 'libglib-2.0.so.*')
8 | # Fixes for Fedora 29:
9 | remove_shared_libraries('libfreetype.so.*', 'libssl.so.*')
10 | # PyInstaller 3.4 includes the library below when on Python 3.6.
11 | # (Interestingly, it does not package it on Python 3.5.) This leads to a lot
12 | # of Fontconfig-related errors when starting the frozen app. Further,
13 | # starting the app takes ages. Removing the library fixes this:
14 | remove_shared_libraries('libfontconfig.so.*')
--------------------------------------------------------------------------------
/ppg/_server.py:
--------------------------------------------------------------------------------
1 | from urllib.error import HTTPError
2 | from urllib.request import Request, urlopen
3 |
4 | import json
5 |
6 | _API_URL = 'https://build-system.fman.io/api/'
7 |
8 | def post_json(path, data, encoding='utf-8'):
9 | # We could just use the requests library. But it doesn't pay off to add the
10 | # whole dependency for just this one function.
11 | request = Request(_API_URL + path)
12 | request.add_header('Content-Type', 'application/json; charset=' + encoding)
13 | data_bytes = json.dumps(data).encode(encoding)
14 | request.add_header('Content-Length', len(data_bytes))
15 | try:
16 | with urlopen(request, data_bytes) as response:
17 | return response.getcode(), response.read().decode(encoding)
18 | except HTTPError as e:
19 | return e.getcode(), e.fp.read().decode(encoding)
--------------------------------------------------------------------------------
/examples/multi-screens/src/main/python/views/AuthScreen.py:
--------------------------------------------------------------------------------
1 |
2 | from ppg_runtime.application_context import Pydux, PPGLifeCycle, init_lifecycle
3 | from PySide6.QtWidgets import QWidget, QVBoxLayout, QLineEdit, QPushButton, QLabel
4 |
5 | @init_lifecycle
6 | class AuthScreen(QWidget, PPGLifeCycle, Pydux):
7 |
8 | def component_will_mount(self):
9 | self.subscribe_to_store(self)
10 |
11 | def render_(self):
12 | layout = QVBoxLayout()
13 | layout.addWidget(QLabel('Auth Screen', self))
14 | layout.addWidget(QLineEdit(self, placeholderText='Username'))
15 | layout.addWidget(QLineEdit(self, placeholderText='Password', echoMode=QLineEdit.EchoMode.Password))
16 | layout.addWidget(QPushButton('Login', self, clicked=self.handle_login))
17 | self.setLayout(layout)
18 |
19 | def handle_login(self):
20 | self.update_store({'current_screen_index': 1})
21 |
22 |
23 |
--------------------------------------------------------------------------------
/ppg/installer/mac/create-dmg/examples/01-main-example/sample:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | APP_NAME="Application"
4 | DMG_FILE_NAME="${APP_NAME}-Installer.dmg"
5 | VOLUME_NAME="${APP_NAME} Installer"
6 | SOURCE_FOLDER_PATH="source_folder/"
7 |
8 | if [[ -e ../../create-dmg ]]; then
9 | # We're running from the repo
10 | CREATE_DMG=../../create-dmg
11 | else
12 | # We're running from an installation under a prefix
13 | CREATE_DMG=../../../../bin/create-dmg
14 | fi
15 |
16 | # Since create-dmg does not clobber, be sure to delete previous DMG
17 | [[ -f "${DMG_FILE_NAME}" ]] && rm "${DMG_FILE_NAME}"
18 |
19 | # Create the DMG
20 | $CREATE_DMG \
21 | --volname "${VOLUME_NAME}" \
22 | --background "installer_background.png" \
23 | --window-pos 200 120 \
24 | --window-size 800 400 \
25 | --icon-size 100 \
26 | --icon "${APP_NAME}.app" 200 190 \
27 | --hide-extension "${APP_NAME}.app" \
28 | --app-drop-link 600 185 \
29 | "${DMG_FILE_NAME}" \
30 | "${SOURCE_FOLDER_PATH}"
31 |
--------------------------------------------------------------------------------
/ppg_runtime/ReactiveWidgets/__init__.py:
--------------------------------------------------------------------------------
1 | from .check_box import ReactiveCheckBox
2 | from .combo_box import ReactiveComboBox
3 | from .label import ReactiveLabel
4 | from .line_edit import ReactiveLineEdit
5 | from .slider import ReactiveSlider
6 | from .spin_box import ReactiveSpinBox
7 | from .text_edit import ReactiveTextEdit
8 |
9 | __all__ = [
10 | "ReactiveCheckBox",
11 | "ReactiveComboBox",
12 | "ReactiveLabel",
13 | "ReactiveLineEdit",
14 | "ReactiveSlider",
15 | "ReactiveSpinBox",
16 | "ReactiveTextEdit",
17 | ]
18 |
19 | # alias for backward compatibility
20 | CheckBox = ReactiveCheckBox
21 | QCheckBox = ReactiveCheckBox
22 | ComboBox = ReactiveComboBox
23 | QComboBox = ReactiveComboBox
24 | Label = ReactiveLabel
25 | QLabel = ReactiveLabel
26 | LineEdit = ReactiveLineEdit
27 | QLineEdit = ReactiveLineEdit
28 | Slider = ReactiveSlider
29 | QSlider = ReactiveSlider
30 | SpinBox = ReactiveSpinBox
31 | QSpinBox = ReactiveSpinBox
32 | TextEdit = ReactiveTextEdit
33 | QTextEdit = ReactiveTextEdit
--------------------------------------------------------------------------------
/ppg/repo/ubuntu.py:
--------------------------------------------------------------------------------
1 | from ppg import path
2 | from ppg._gpg import preset_gpg_passphrase
3 | from ppg.resources import copy_with_filtering
4 | from ppg_runtime._source import default_path
5 | from os import makedirs
6 | from os.path import exists
7 | from shutil import rmtree
8 | from subprocess import check_call, DEVNULL
9 |
10 | def create_repo_ubuntu():
11 | dest_dir = path('target/repo')
12 | tmp_dir = path('target/repo-tmp')
13 | if exists(dest_dir):
14 | rmtree(dest_dir)
15 | if exists(tmp_dir):
16 | rmtree(tmp_dir)
17 | makedirs(tmp_dir)
18 | distr_file = 'src/repo/ubuntu/distributions'
19 | distr_path = path(distr_file)
20 | if not exists(distr_path):
21 | distr_path = default_path(distr_file)
22 | copy_with_filtering(distr_path, tmp_dir, files_to_filter=[distr_path])
23 | preset_gpg_passphrase()
24 | check_call([
25 | 'reprepro', '-b', dest_dir, '--confdir', tmp_dir,
26 | 'includedeb', 'stable', path('target/${installer}')
27 | ], stdout=DEVNULL)
--------------------------------------------------------------------------------
/ppg/installer/mac/create-dmg/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile for Cowsay
2 |
3 | PACKAGE_TARNAME = create-dmg
4 |
5 | prefix = /usr/local
6 | exec_prefix = ${prefix}
7 | bindir = ${exec_prefix}/bin
8 | datarootdir = ${prefix}/share
9 | datadir = ${datarootdir}
10 | docdir = ${datarootdir}/doc/${PACKAGE_TARNAME}
11 | sysconfdir = ${prefix}/etc
12 | mandir=${datarootdir}/man
13 | srcdir = .
14 |
15 | SHELL = /bin/sh
16 | INSTALL = install
17 | INSTALL_PROGRAM = $(INSTALL)
18 | INSTALL_DATA = ${INSTALL} -m 644
19 |
20 | .PHONY: install uninstall
21 |
22 | install: create-dmg
23 | $(INSTALL) -d $(DESTDIR)$(prefix)
24 | $(INSTALL) -d $(DESTDIR)$(bindir)
25 | $(INSTALL_PROGRAM) create-dmg $(DESTDIR)$(bindir)/create-dmg
26 | $(INSTALL) -d $(DESTDIR)$(datadir)/$(PACKAGE_TARNAME)
27 | cp -R support $(DESTDIR)$(datadir)/$(PACKAGE_TARNAME)
28 | cp -R examples $(DESTDIR)$(datadir)/$(PACKAGE_TARNAME)
29 | cp -R tests $(DESTDIR)$(datadir)/$(PACKAGE_TARNAME)
30 |
31 | uninstall:
32 | rm -f $(DESTDIR)$(bindir)/create-dmg
33 | rm -rf $(DESTDIR)$(datadir)/$(PACKAGE_TARNAME)
34 |
--------------------------------------------------------------------------------
/ppg_runtime/_fbs.py:
--------------------------------------------------------------------------------
1 | from ppg_runtime import platform
2 | from ppg_runtime.platform import is_ubuntu, is_linux, is_arch_linux, is_fedora
3 |
4 | def get_core_settings(project_dir):
5 | return {
6 | 'project_dir': project_dir
7 | }
8 |
9 | def get_default_profiles():
10 | result = ['base']
11 | # The "secret" profile lets the user store sensitive settings such as
12 | # passwords in src/build/settings/secret.json. When using Git, the user can
13 | # exploit this by adding secret.json to .gitignore, thus preventing it from
14 | # being uploaded to services such as GitHub.
15 | result.append('secret')
16 | result.append(platform.name().lower())
17 | if is_linux():
18 | if is_ubuntu():
19 | result.append('ubuntu')
20 | elif is_arch_linux():
21 | result.append('arch')
22 | elif is_fedora():
23 | result.append('fedora')
24 | return result
25 |
26 | def filter_public_settings(settings):
27 | return {k: settings[k] for k in settings['public_settings']}
--------------------------------------------------------------------------------
/ppg/repo/fedora.py:
--------------------------------------------------------------------------------
1 | from ppg import path
2 | from ppg.resources import copy_with_filtering
3 | from ppg_runtime._source import default_path
4 | from os import makedirs, rename
5 | from os.path import exists
6 | from shutil import rmtree, copy
7 | from subprocess import check_call, DEVNULL
8 |
9 | def create_repo_fedora():
10 | if exists(path('target/repo')):
11 | rmtree(path('target/repo'))
12 | makedirs(path('target/repo/${version}'))
13 | copy(path('target/${installer}'), path('target/repo/${version}'))
14 | check_call(['createrepo_c', '.'], cwd=(path('target/repo')), stdout=DEVNULL)
15 | repo_file = path('src/repo/fedora/${app_name}.repo')
16 | use_default = not exists(repo_file)
17 | if use_default:
18 | repo_file = default_path('src/repo/fedora/AppName.repo')
19 | copy_with_filtering(
20 | repo_file, path('target/repo'), files_to_filter=[repo_file]
21 | )
22 | if use_default:
23 | rename(
24 | path('target/repo/AppName.repo'),
25 | path('target/repo/${app_name}.repo')
26 | )
--------------------------------------------------------------------------------
/ppg/freeze/linux.py:
--------------------------------------------------------------------------------
1 | from ppg import path
2 | from ppg.freeze import _generate_resources, run_pyinstaller
3 | from glob import glob
4 | from os import remove
5 | from shutil import copy
6 |
7 | def freeze_linux(debug=False):
8 | run_pyinstaller(debug=debug)
9 | _generate_resources()
10 | copy(path('src/main/icons/Icon.ico'), path('${freeze_dir}'))
11 | # For some reason, PyInstaller packages libstdc++.so.6 even though it is
12 | # available on most Linux distributions. If we include it and run our app on
13 | # a different Ubuntu version, then Popen(...) calls fail with errors
14 | # "GLIBCXX_... not found" or "CXXABI_..." not found. So ensure we don't
15 | # package the file, so that the respective system's compatible version is
16 | # used:
17 | remove_shared_libraries(
18 | 'libstdc++.so.*', 'libtinfo.so.*', 'libreadline.so.*', 'libdrm.so.*'
19 | )
20 |
21 | def remove_shared_libraries(*filename_patterns):
22 | for pattern in filename_patterns:
23 | for file_path in glob(path('${freeze_dir}/' + pattern)):
24 | remove(file_path)
--------------------------------------------------------------------------------
/ppg/_defaults/src/freeze/mac/Contents/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleExecutable
6 | ${app_name}
7 | CFBundleDisplayName
8 | ${app_name}
9 | CFBundlePackageType
10 | APPL
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleIconFile
14 | Icon.icns
15 | CFBundleIdentifier
16 | ${mac_bundle_identifier}
17 | LSBackgroundOnly
18 | 0
19 | CFBundleShortVersionString
20 | ${version}
21 | CFBundleVersion
22 | ${version}
23 | CFBundleName
24 | ${app_name}
25 |
26 | NSPrincipalClass
27 | NSApplication
28 |
29 |
--------------------------------------------------------------------------------
/ppg/_aws.py:
--------------------------------------------------------------------------------
1 | from os.path import relpath, join
2 |
3 | import os
4 |
5 | def upload_folder_contents(dir_path, dest_path, bucket, key, secret):
6 | result = []
7 | for file_path in _iter_files_recursive(dir_path):
8 | file_relpath = relpath(file_path, dir_path)
9 | # Replace backslashes on Windows by forward slashes:
10 | file_relpath = file_relpath.replace(os.sep, '/')
11 | file_dest = dest_path + '/' + file_relpath
12 | upload_file(file_path, file_dest, bucket, key, secret)
13 | result.append(file_dest)
14 | return result
15 |
16 | def upload_file(file_path, dest_path, bucket, key, secret):
17 | # Import late to not make boto3 a dependency of ppg only when the upload
18 | # functionality is actually used.
19 | import boto3
20 | s3 = boto3.resource(
21 | 's3', aws_access_key_id=key, aws_secret_access_key=secret
22 | )
23 | s3.Bucket(bucket).upload_file(file_path, dest_path)
24 |
25 | def _iter_files_recursive(dir_path):
26 | for subdir_path, dir_names, file_names in os.walk(dir_path):
27 | for file_name in file_names:
28 | yield join(subdir_path, file_name)
--------------------------------------------------------------------------------
/ppg/repo/arch.py:
--------------------------------------------------------------------------------
1 | from ppg import path, SETTINGS
2 | from ppg_runtime import FbsError
3 | from os import makedirs
4 | from os.path import exists, join
5 | from shutil import rmtree, copy
6 | from subprocess import check_call, DEVNULL
7 |
8 | def create_repo_arch():
9 | if not exists(path('target/${installer}.sig')):
10 | raise FbsError(
11 | 'Installer does not exist or is not signed. Maybe you need to '
12 | 'run:\n'
13 | ' ppg signinst'
14 | )
15 | dest_dir = path('target/repo')
16 | if exists(dest_dir):
17 | rmtree(dest_dir)
18 | makedirs(dest_dir)
19 | app_name = SETTINGS['app_name']
20 | pkg_file = path('target/${installer}')
21 | pkg_file_versioned = '%s-%s.pkg.tar.xz' % (app_name, SETTINGS['version'])
22 | copy(pkg_file, join(dest_dir, pkg_file_versioned))
23 | copy(pkg_file + '.sig', join(dest_dir, pkg_file_versioned + '.sig'))
24 | check_call(
25 | ['repo-add', '%s.db.tar.gz' % app_name, pkg_file_versioned],
26 | cwd=dest_dir, stdout=DEVNULL
27 | )
28 | # Ensure the permissions are correct if uploading to a server:
29 | check_call(['chmod', 'g-w', '-R', dest_dir])
--------------------------------------------------------------------------------
/ppg/installer/mac/create-dmg/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2008-2014 Andrey Tarantsov
4 | Copyright (c) 2020 Andrew Janke
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 |
--------------------------------------------------------------------------------
/ppg/freeze/hooks/hook-shiboken2.py:
--------------------------------------------------------------------------------
1 | from glob import glob
2 | from os.path import dirname, relpath, join
3 |
4 | import PySide2
5 |
6 | support = None
7 |
8 | if PySide2.__version__ == "5.12.2":
9 | import PySide2.support as support
10 | else:
11 | try:
12 | # PySide2 < 5.12.2
13 | import shiboken2.support as support
14 | except ImportError:
15 | # PySide2 >= 5.12.3
16 | pass
17 |
18 | if support is not None:
19 | """
20 | This should give roughly the same results as:
21 |
22 | from PyInstaller.utils.hooks import collect_data_files
23 | datas = collect_data_files(
24 | 'shiboken2', include_py_files=True, subdir='support'
25 | )
26 |
27 | The reason we don't do it this way is that it would add a dynamic link to
28 | PyInstaller, and thus force the GPL on fbs, preventing it from being
29 | licensed under different terms (such as a commercial license).
30 | """
31 | _base_dir = dirname(support.__file__)
32 | _python_files = glob(join(_base_dir, '**', '*.py'), recursive=True)
33 | _site_packages = dirname(dirname(_base_dir))
34 | datas = [(f, relpath(dirname(f), _site_packages)) for f in _python_files]
35 |
36 | hiddenimports = ['typing']
--------------------------------------------------------------------------------
/ppg/freeze/hooks/hook-shiboken6.py:
--------------------------------------------------------------------------------
1 | from glob import glob
2 | from os.path import dirname, relpath, join
3 |
4 | import PySide6
5 |
6 | support = None
7 |
8 | if PySide6.__version__ == "6.0.0":
9 | import PySide6.support as support
10 | else:
11 | try:
12 | # PySide2 < 5.12.2
13 | import shiboken6.support as support
14 | except ImportError:
15 | # PySide2 >= 5.12.3
16 | pass
17 |
18 | if support is not None:
19 | """
20 | This should give roughly the same results as:
21 |
22 | from PyInstaller.utils.hooks import collect_data_files
23 | datas = collect_data_files(
24 | 'shiboken2', include_py_files=True, subdir='support'
25 | )
26 |
27 | The reason we don't do it this way is that it would add a dynamic link to
28 | PyInstaller, and thus force the GPL on fbs, preventing it from being
29 | licensed under different terms (such as a commercial license).
30 | """
31 | _base_dir = dirname(support.__file__)
32 | _python_files = glob(join(_base_dir, '**', '*.py'), recursive=True)
33 | _site_packages = dirname(dirname(_base_dir))
34 | datas = [(f, relpath(dirname(f), _site_packages)) for f in _python_files]
35 |
36 | hiddenimports = ['typing']
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ppg",
3 | "version": "1.2.0",
4 | "description": "PPG: A powerful app generator simplifying development with Python and Qt.",
5 | "license": "GPL-3.0-only",
6 | "author": "Neuri",
7 | "author_email": "alfredo@neuri.ai",
8 | "homepage": "https://ppg.neuri.ai",
9 | "repository": {
10 | "type": "git",
11 | "url": "https://github.com/neuri-ai/PPG"
12 | },
13 | "keywords": [
14 | "ppg",
15 | "python",
16 | "pyqt",
17 | "pyside",
18 | "desktop",
19 | "application",
20 | "generator",
21 | "cli",
22 | "component",
23 | "state management",
24 | "pydux",
25 | "fbs",
26 | "react",
27 | "redux",
28 | "app development",
29 | "modular",
30 | "scalable",
31 | "lifecycle",
32 | "component-based",
33 | "pyside6",
34 | "pyqt6",
35 | "pyqt5",
36 | "pyqt6 generator",
37 | "pyqt5 generator",
38 | "pyside6 generator",
39 | "pyside5 generator",
40 | "pyqt6 app",
41 | "pyqt5 app",
42 | "pyside6 app",
43 | "pyside5 app",
44 | "app generator",
45 | "redux"
46 | ],
47 | "platforms": [
48 | "MacOS",
49 | "Windows",
50 | "Debian",
51 | "Fedora",
52 | "CentOS",
53 | "Arch",
54 | "Raspbian"
55 | ]
56 | }
--------------------------------------------------------------------------------
/ppg/installer/mac/__init__.py:
--------------------------------------------------------------------------------
1 | import platform
2 | from ppg import path, SETTINGS
3 | from ppg_runtime.platform import is_mac
4 | from os import replace, remove
5 | from os.path import join, dirname, exists
6 | from subprocess import check_call, DEVNULL
7 |
8 |
9 | def create_installer_mac():
10 | app_name = SETTINGS['app_name']
11 | dest = path('target/${installer}')
12 | dest_existed = exists(dest)
13 | if dest_existed:
14 | dest_bu = dest + '.bu'
15 | replace(dest, dest_bu)
16 | try:
17 | pdata = [
18 | join(dirname(__file__), 'create-dmg', 'create-dmg'),
19 | '--volname', app_name,
20 | '--app-drop-link', '170', '10',
21 | '--icon', app_name + '.app', '0', '10',
22 | dest,
23 | path('${freeze_dir}')
24 | ]
25 | if is_mac():
26 | major, minor = platform.mac_ver()[0].split('.')[:2]
27 | if (int(major) == 10 and int(minor) >= 15) or int(major) >= 11:
28 | pdata.insert(1, '--no-internet-enable')
29 | check_call(pdata, stdout=DEVNULL)
30 | except:
31 | if dest_existed:
32 | replace(dest_bu, dest)
33 | raise
34 | else:
35 | if dest_existed:
36 | remove(dest_bu)
37 |
--------------------------------------------------------------------------------
/ppg_runtime/application_context/PyQt6.py:
--------------------------------------------------------------------------------
1 | """
2 | Earlier fbs versions had the following code:
3 |
4 | try:
5 | from PyQt5 import ...
6 | except ImportError:
7 | from PySide2 import ...
8 |
9 | This lead to problems when both PyQt5 and PySide2 were on PYTHONPATH:
10 |
11 | 1) PyInstaller packaged both (!) libraries because it saw both imports.
12 | 2) The above made fbs always use PyQt5. But if the user's app uses PySide2,
13 | then PySide2 and PyQt5 classes / code would be mixed.
14 | 3) It wasn't clear (or deterministic, really) which Python binding took
15 | precedence. For instance, PyQt5 and PySide2 set different QML search paths.
16 |
17 | To fix this problems, the above code was split into separate files: One that
18 | contains all PyQt5 imports, and another that contains all PySide2 imports. The
19 | user is supposed to import precisely one of the two. This makes PyInstaller
20 | only package the one necessary library, and prevents the above problems.
21 | """
22 |
23 | from . import _ApplicationContext, _QtBinding, cached_property
24 | from PyQt6.QtGui import QIcon
25 | from PyQt6.QtWidgets import QApplication
26 | from PyQt6.QtNetwork import QAbstractSocket
27 | class ApplicationContext(_ApplicationContext):
28 | @cached_property
29 | def _qt_binding(self):
30 | return _QtBinding(QApplication, QIcon, QAbstractSocket)
--------------------------------------------------------------------------------
/ppg/_defaults/src/build/settings/base.json:
--------------------------------------------------------------------------------
1 | {
2 | "freeze_dir": "target/${app_name}",
3 | "test_dirs": [
4 | "src/unittest/python",
5 | "src/integrationtest/python"
6 | ],
7 | "files_to_filter": [
8 | "src/build/docker/ubuntu/.bashrc", "src/build/docker/ubuntu/Dockerfile",
9 | "src/build/docker/arch/.bashrc", "src/build/docker/arch/Dockerfile",
10 | "src/build/docker/fedora/.bashrc", "src/build/docker/fedora/Dockerfile",
11 | "src/build/docker/fedora/.rpmmacros"
12 | ],
13 | "hidden_imports": [],
14 | "extra_pyinstaller_args": [],
15 | "public_settings": ["app_name", "author", "version", "environment"],
16 | "docker_images": {
17 | "ubuntu": {
18 | "build_files": ["requirements/", "src/sign/linux/"],
19 | "build_args": {
20 | "requirements": "ubuntu.txt"
21 | }
22 | },
23 | "arch": {
24 | "build_files": ["requirements/", "src/sign/linux/"],
25 | "build_args": {
26 | "requirements": "arch.txt"
27 | }
28 | },
29 | "fedora": {
30 | "build_files": ["requirements/", "src/sign/linux/"],
31 | "build_args": {
32 | "requirements": "fedora.txt"
33 | }
34 | }
35 | },
36 | "release": false,
37 | "environment": "local"
38 | }
--------------------------------------------------------------------------------
/ppg_runtime/application_context/PyQt5.py:
--------------------------------------------------------------------------------
1 | """
2 | Earlier fbs versions had the following code:
3 |
4 | try:
5 | from PyQt5 import ...
6 | except ImportError:
7 | from PySide2 import ...
8 |
9 | This lead to problems when both PyQt5 and PySide2 were on PYTHONPATH:
10 |
11 | 1) PyInstaller packaged both (!) libraries because it saw both imports.
12 | 2) The above made fbs always use PyQt5. But if the user's app uses PySide2,
13 | then PySide2 and PyQt5 classes / code would be mixed.
14 | 3) It wasn't clear (or deterministic, really) which Python binding took
15 | precedence. For instance, PyQt5 and PySide2 set different QML search paths.
16 |
17 | To fix this problems, the above code was split into separate files: One that
18 | contains all PyQt5 imports, and another that contains all PySide2 imports. The
19 | user is supposed to import precisely one of the two. This makes PyInstaller
20 | only package the one necessary library, and prevents the above problems.
21 | """
22 |
23 | from . import _ApplicationContext, _QtBinding, cached_property
24 | from PyQt5.QtGui import QIcon
25 | from PyQt5.QtWidgets import QApplication
26 | from PyQt5.QtNetwork import QAbstractSocket
27 | from . import PPGLifeCycle
28 |
29 | class ApplicationContext(_ApplicationContext):
30 | @cached_property
31 | def _qt_binding(self):
32 | return _QtBinding(QApplication, QIcon, QAbstractSocket)
--------------------------------------------------------------------------------
/ppg_runtime/application_context/PySide2.py:
--------------------------------------------------------------------------------
1 | """
2 | Earlier fbs versions had the following code:
3 |
4 | try:
5 | from PyQt5 import ...
6 | except ImportError:
7 | from PySide2 import ...
8 |
9 | This lead to problems when both PyQt5 and PySide2 were on PYTHONPATH:
10 |
11 | 1) PyInstaller packaged both (!) libraries because it saw both imports.
12 | 2) The above made fbs always use PyQt5. But if the user's app uses PySide2,
13 | then PySide2 and PyQt5 classes / code would be mixed.
14 | 3) It wasn't clear (or deterministic, really) which Python binding took
15 | precedence. For instance, PyQt5 and PySide2 set different QML search paths.
16 |
17 | To fix this problems, the above code was split into separate files: One that
18 | contains all PyQt5 imports, and another that contains all PySide2 imports. The
19 | user is supposed to import precisely one of the two. This makes PyInstaller
20 | only package the one necessary library, and prevents the above problems.
21 | """
22 |
23 | from . import _ApplicationContext, _QtBinding, cached_property
24 | from PySide2.QtGui import QIcon
25 | from PySide2.QtWidgets import QApplication
26 | from PySide2.QtNetwork import QAbstractSocket
27 | from . import PPGLifeCycle
28 |
29 | class ApplicationContext(_ApplicationContext):
30 | @cached_property
31 | def _qt_binding(self):
32 | return _QtBinding(QApplication, QIcon, QAbstractSocket)
--------------------------------------------------------------------------------
/ppg/freeze/hooks/hook-PySide2.py:
--------------------------------------------------------------------------------
1 | """
2 | This hook is in particular required to prevent errors with the following code
3 | under PySide2 5.12.1:
4 |
5 | from PySide2.QtWidgets import QApplication
6 | print(QApplication.__signature__)
7 |
8 | It should print
9 |
10 | [, ]
11 |
12 | Without this hook, it prints a lot of output of the form
13 |
14 | parser.py:191: RuntimeWarning: pyside_type_init:
15 | UNRECOGNIZED: 'PySide2.QtGui....'
16 |
17 | Then, it prints Signature('QStringList') instead of the above ...(List).
18 | """
19 |
20 | from glob import glob
21 | from os.path import dirname, relpath, join
22 |
23 | import PySide2.support
24 |
25 | """
26 | This should give roughly the same results as:
27 |
28 | from PyInstaller.utils.hooks import collect_data_files
29 | datas = collect_data_files(
30 | 'PySide2', include_py_files=True, subdir='support'
31 | )
32 |
33 | The reason we don't do it this way is that it would add a dynamic link to
34 | PyInstaller, and thus force the GPL on fbs, preventing it from being licensed
35 | under different terms (such as a commercial license).
36 | """
37 | _base_dir = dirname(PySide2.support.__file__)
38 | _python_files = glob(join(_base_dir, '**', '*.py'), recursive=True)
39 | _site_packages = dirname(dirname(_base_dir))
40 | datas = [(f, relpath(dirname(f), _site_packages)) for f in _python_files]
--------------------------------------------------------------------------------
/ppg/freeze/hooks/hook-PySide6.py:
--------------------------------------------------------------------------------
1 | """
2 | This hook is in particular required to prevent errors with the following code
3 | under PySide6 6.0.0:
4 |
5 | from PySide6.QtWidgets import QApplication
6 | print(QApplication.__signature__)
7 |
8 | It should print
9 |
10 | [, ]
11 |
12 | Without this hook, it prints a lot of output of the form
13 |
14 | parser.py:191: RuntimeWarning: pyside_type_init:
15 | UNRECOGNIZED: 'PySide6.QtGui....'
16 |
17 | Then, it prints Signature('QStringList') instead of the above ...(List).
18 | """
19 |
20 | from glob import glob
21 | from os.path import dirname, relpath, join
22 |
23 | import PySide6.support
24 |
25 | """
26 | This should give roughly the same results as:
27 |
28 | from PyInstaller.utils.hooks import collect_data_files
29 | datas = collect_data_files(
30 | 'PySide2', include_py_files=True, subdir='support'
31 | )
32 |
33 | The reason we don't do it this way is that it would add a dynamic link to
34 | PyInstaller, and thus force the GPL on fbs, preventing it from being licensed
35 | under different terms (such as a commercial license).
36 | """
37 | _base_dir = dirname(PySide6.support.__file__)
38 | _python_files = glob(join(_base_dir, '**', '*.py'), recursive=True)
39 | _site_packages = dirname(dirname(_base_dir))
40 | datas = [(f, relpath(dirname(f), _site_packages)) for f in _python_files]
--------------------------------------------------------------------------------
/ppg_runtime/application_context/PySide6.py:
--------------------------------------------------------------------------------
1 | """
2 | Earlier fbs versions had the following code:
3 |
4 | try:
5 | from PyQt5 import ...
6 | except ImportError:
7 | from PySide2 import ...
8 |
9 | This lead to problems when both PyQt5 and PySide2 were on PYTHONPATH:
10 |
11 | 1) PyInstaller packaged both (!) libraries because it saw both imports.
12 | 2) The above made fbs always use PyQt5. But if the user's app uses PySide2,
13 | then PySide2 and PyQt5 classes / code would be mixed.
14 | 3) It wasn't clear (or deterministic, really) which Python binding took
15 | precedence. For instance, PyQt5 and PySide2 set different QML search paths.
16 |
17 | To fix this problems, the above code was split into separate files: One that
18 | contains all PyQt5 imports, and another that contains all PySide2 imports. The
19 | user is supposed to import precisely one of the two. This makes PyInstaller
20 | only package the one necessary library, and prevents the above problems.
21 | """
22 |
23 | from . import _ApplicationContext, _QtBinding, cached_property
24 | from PySide6.QtGui import QIcon
25 | from PySide6.QtWidgets import QApplication
26 | from PySide6.QtNetwork import QAbstractSocket
27 | from . import PPGLifeCycle
28 |
29 |
30 |
31 | class ApplicationContext(_ApplicationContext):
32 | @cached_property
33 | def _qt_binding(self):
34 | return _QtBinding(QApplication, QIcon, QAbstractSocket)
35 |
36 |
--------------------------------------------------------------------------------
/ppg/builtin_commands/_account.py:
--------------------------------------------------------------------------------
1 | from ppg import path, _server
2 | from ppg.builtin_commands import prompt_for_value, require_existing_project
3 | from ppg.builtin_commands._util import update_json, SECRET_JSON
4 | from ppg.cmdline import command
5 |
6 | import logging
7 |
8 | _LOG = logging.getLogger(__name__)
9 |
10 | @command
11 | def register():
12 | """
13 | Create an account for uploading your files
14 | """
15 | require_existing_project()
16 | username = prompt_for_value('Username')
17 | email = prompt_for_value('Real email')
18 | password = prompt_for_value('Password', password=True)
19 | print('')
20 | status, text = _server.post_json('register', {
21 | 'username': username, 'email': email, 'password': password
22 | })
23 | if status == 201:
24 | if text:
25 | _LOG.info(text)
26 | _login(username, password)
27 | else:
28 | _LOG.error('Could not register:\n' + text)
29 |
30 | @command
31 | def login():
32 | """
33 | Save your account details to secret.json
34 | """
35 | require_existing_project()
36 | username = prompt_for_value('Username')
37 | password = prompt_for_value('Password', password=True)
38 | print('')
39 | _login(username, password)
40 |
41 | def _login(username, password):
42 | update_json(path(SECRET_JSON), {'fbs_user': username, 'fbs_pass': password})
43 | _LOG.info('Saved your username and password to %s.', SECRET_JSON)
--------------------------------------------------------------------------------
/ppg/freeze/ubuntu.py:
--------------------------------------------------------------------------------
1 | from ppg.freeze.linux import freeze_linux, remove_shared_libraries
2 |
3 | def freeze_ubuntu(debug=False):
4 | freeze_linux(debug)
5 | # When we build on Ubuntu on 14.04 and run on 17.10, the app fails to start
6 | # with the following error:
7 | #
8 | # > This application failed to start because it could not find or load the
9 | # > Qt platform plugin "xcb" in "". Available platform plugins are:
10 | # > eglfs, linuxfb, minimal, minimalegl, offscreen, vnc, xcb.
11 | #
12 | # Interestingly, the error does not occur when building on Ubuntu 16.04.
13 | # The difference between the two build outputs seems to be
14 | # libgpg-error.so.0. Removing it fixes the problem:
15 | remove_shared_libraries('libgpg-error.so.*')
16 | # libgtk-3.so is present on every Ubuntu system. Make sure we don't ship it
17 | # to avoid incompatibilities. In particular, running the frozen app with
18 | # libgtk-3.so from Ubuntu 14 on Ubuntu 16 produces many Gtk warnings
19 | # "Theme parsing error".
20 | remove_shared_libraries('libgtk-3.so.*')
21 | # We also don't want to ship libgio-2.0.so because it is usually present.
22 | # What's more, if we ship libgio without libgtk, then segmentation faults
23 | # occur when freezing on Ubuntu 14 and running on Ubuntu 16. The reason for
24 | # this is that libgio depends on libgtk. Because we don't ship libgtk, this
25 | # loads the user's libgtk, which is incompatible between Ubuntu 14 and 16.
26 | remove_shared_libraries('libgio-2.0.so.*')
--------------------------------------------------------------------------------
/ppg/installer/mac/create-dmg/doc-project/Developer Notes.md:
--------------------------------------------------------------------------------
1 | # create-dmg Developer Notes
2 |
3 | ## Repo layout
4 |
5 | - `create-dmg` in the root of the repo is the main program
6 | - `support/` contains auxiliary scripts used by `create-dmg`; it must be at that relative position to `create-dmg`
7 | - `builder/` contains ????
8 | - `examples/` contains user-facing examples
9 | - `tests/` contains regression tests for developers
10 | - `doc-project/` contains developer-facing documentation about this project
11 |
12 | ### tests/
13 |
14 | The `tests/` folder contains regression tests for developers.
15 |
16 | Each test is in its own subfolder.
17 | Each subfolder name should start with a 3-digit number that is the number of the corresponding bug report in create-dmg's GitHub issue tracker.
18 |
19 | The tests are to be run manually, with the results examined manually.
20 | There's no automated script to run them as a suite and check their results.
21 | That might be nice to have.
22 |
23 | ### examples/
24 |
25 | Each example is in its own subfolder.
26 | The subfolder prefix number is arbitrary; these numbers should roughly be in order of "advancedness" of examples, so it makes sense for users to go through them in order.
27 |
28 | ## Versioning
29 |
30 | As of May 2020, we're using SemVer versioning.
31 | The old version numbers were 4-parters, like "1.0.0.7".
32 | Now we use 3-part SemVer versions, like "1.0.8".
33 | This change happened after version 1.0.0.7; 1.0.8 is the next release after 1.0.0.7.
34 |
35 | The suffix "-SNAPSHOT" is used to denote a version that is still under development.
36 |
--------------------------------------------------------------------------------
/examples/pydux/src/main/python/main.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from ppg_runtime.application_context.PySide6 import ApplicationContext
3 | from ppg_runtime.application_context import PPGLifeCycle, Pydux, init_lifecycle
4 | from ppg_runtime.application_context.devtools.reloader import hot_reloading
5 | from ppg_runtime.application_context.utils import app_is_frozen
6 | from PySide6.QtWidgets import QMainWindow, QTextEdit, QLabel
7 |
8 |
9 | @init_lifecycle
10 | @hot_reloading
11 | class Pydux(QMainWindow, PPGLifeCycle, Pydux):
12 | def component_will_mount(self):
13 | self.subscribe_to_store(self)
14 | self.set_schema({
15 | "greeting": str
16 | })
17 |
18 | def render_(self):
19 | self.label = QLabel('Hello World!', self)
20 | self.line = QTextEdit(self, placeholderText='Type something...', textChanged=lambda: self.on_text_changed(self.line.toPlainText()))
21 |
22 | def on_text_changed(self, text):
23 | self.update_store({"greeting": text})
24 |
25 | def responsive_UI(self):
26 | self.setMinimumSize(640, 480)
27 | self.label.move(20, 20)
28 | self.line.setGeometry(20, 120, 600, 300)
29 |
30 | def on_store_change(self, _):
31 | self.label.setText(self.store.get("greeting", ""))
32 | self.label.adjustSize()
33 |
34 |
35 | if __name__ == '__main__':
36 | appctxt = ApplicationContext()
37 | window = Pydux()
38 | if not app_is_frozen():
39 | window._init_hot_reload_system(__file__)
40 | window.show()
41 | exec_func = getattr(appctxt.app, 'exec', appctxt.app.exec_)
42 | sys.exit(exec_func())
--------------------------------------------------------------------------------
/ppg/freeze/mac.py:
--------------------------------------------------------------------------------
1 | from ppg import path, SETTINGS
2 | from ppg.freeze import _generate_resources, run_pyinstaller
3 | from ppg.resources import get_icons
4 | from os import makedirs, unlink, rename, symlink
5 | from os.path import exists
6 | from shutil import copy, rmtree
7 | from subprocess import run
8 |
9 | def freeze_mac(debug=False):
10 | if not exists(path('target/Icon.icns')):
11 | _generate_iconset()
12 | run(['iconutil', '-c', 'icns', path('target/Icon.iconset')], check=True)
13 | args = []
14 | if not (debug or SETTINGS['show_console_window']):
15 | args.append('--windowed')
16 | args.extend(['--icon', path('target/Icon.icns')])
17 | bundle_identifier = SETTINGS['mac_bundle_identifier']
18 | if bundle_identifier:
19 | args.extend([
20 | '--osx-bundle-identifier', bundle_identifier
21 | ])
22 | run_pyinstaller(args, debug)
23 | _remove_unwanted_pyinstaller_files()
24 | _generate_resources()
25 |
26 | def _generate_iconset():
27 | makedirs(path('target/Icon.iconset'), exist_ok=True)
28 | for size, scale, icon_path in get_icons():
29 | dest_name = 'icon_%dx%d' % (size, size)
30 | if scale != 1:
31 | dest_name += '@%dx' % scale
32 | dest_name += '.png'
33 | copy(icon_path, path('target/Icon.iconset/' + dest_name))
34 |
35 | def _remove_unwanted_pyinstaller_files():
36 | for unwanted in ('include', 'lib', 'lib2to3'):
37 | try:
38 | unlink(path('${freeze_dir}/Contents/MacOS/' + unwanted))
39 | except FileNotFoundError:
40 | pass
41 | try:
42 | rmtree(path('${freeze_dir}/Contents/Resources/' + unwanted))
43 | except FileNotFoundError:
44 | pass
45 |
46 |
--------------------------------------------------------------------------------
/ppg/upload.py:
--------------------------------------------------------------------------------
1 | from ppg import _server, SETTINGS, path
2 | from ppg._aws import upload_file, upload_folder_contents
3 | from ppg_runtime import FbsError
4 | from ppg_runtime.platform import is_linux
5 | from os.path import basename
6 |
7 | import json
8 |
9 | def _upload_repo(username, password):
10 | status, response = _server.post_json('start_upload', {
11 | 'username': username,
12 | 'password': password
13 | })
14 | unexpected_response = lambda: FbsError(
15 | 'Received unexpected server response %d:\n%s' % (status, response)
16 | )
17 | if status // 2 != 100:
18 | raise unexpected_response()
19 | try:
20 | data = json.loads(response)
21 | except ValueError:
22 | raise unexpected_response()
23 | try:
24 | credentials = data['bucket'], data['key'], data['secret']
25 | except KeyError:
26 | raise unexpected_response()
27 | dest_path = lambda p: username + '/' + SETTINGS['app_name'] + '/' + p
28 | installer = path('target/${installer}')
29 | installer_dest = dest_path(basename(installer))
30 | upload_file(installer, installer_dest, *credentials)
31 | uploaded = [installer_dest]
32 | if is_linux():
33 | repo_dest = dest_path(SETTINGS['repo_subdir'])
34 | uploaded.extend(
35 | upload_folder_contents(path('target/repo'), repo_dest, *credentials)
36 | )
37 | pubkey_dest = dest_path('public-key.gpg')
38 | upload_file(
39 | path('src/sign/linux/public-key.gpg'), pubkey_dest, *credentials
40 | )
41 | uploaded.append(pubkey_dest)
42 | status, response = _server.post_json('complete_upload', {
43 | 'username': username,
44 | 'password': password,
45 | 'files': uploaded
46 | })
47 | if status != 201:
48 | raise unexpected_response()
--------------------------------------------------------------------------------
/ppg/_defaults/src/build/docker/arch/Dockerfile:
--------------------------------------------------------------------------------
1 | # Build on :latest because Arch is a rolling release distribution:
2 | FROM archlinux:latest
3 |
4 | ARG requirements
5 |
6 | # Python 3.7 is the earliest version that won't crash in Arch as of July 2021.
7 | ARG python_version=3.7.11
8 | # List from https://github.com/pyenv/pyenv/wiki#suggested-build-environment:
9 | ARG python_build_deps="base-devel openssl zlib xz git"
10 |
11 | RUN echo 'Server=https://mirror.rackspace.com/archlinux/$repo/os/$arch' > /etc/pacman.d/mirrorlist && \
12 | pacman -Syy
13 |
14 | # Install pyenv:
15 | RUN pacman -S --noconfirm curl git
16 | ENV PYENV_ROOT /root/.pyenv
17 | ENV PATH $PYENV_ROOT/shims:$PYENV_ROOT/bin:$PATH
18 | RUN curl https://pyenv.run | bash
19 | RUN pyenv update
20 |
21 | # Install Python:
22 | RUN echo $python_build_deps | xargs pacman -S --noconfirm
23 | RUN CONFIGURE_OPTS=--enable-shared pyenv install $python_version && \
24 | pyenv global $python_version && \
25 | pyenv rehash
26 |
27 | # Install fpm:
28 | RUN pacman -S --noconfirm ruby ruby-rdoc && \
29 | export PATH=$PATH:$(ruby -e "puts Gem.user_dir")/bin && \
30 | gem update && \
31 | gem install --no-document fpm
32 |
33 | WORKDIR /root/${app_name}
34 |
35 | # Install Python dependencies:
36 | ADD *.txt /tmp/requirements/
37 | RUN pip install --upgrade pip && \
38 | pip install -r "/tmp/requirements/${requirements}"
39 | RUN rm -rf /tmp/requirements/
40 |
41 | # Welcome message, displayed by ~/.bashrc:
42 | ADD motd /etc/motd
43 |
44 | ADD .bashrc /root/.bashrc
45 |
46 | # Import GPG key for code signing the installer:
47 | ADD private-key.gpg public-key.gpg /tmp/
48 | RUN gpg -q --batch --yes --passphrase ${gpg_pass} --import /tmp/private-key.gpg /tmp/public-key.gpg && \
49 | rm /tmp/private-key.gpg /tmp/public-key.gpg
50 |
51 | ADD gpg-agent.conf /root/.gnupg/gpg-agent.conf
52 | RUN gpgconf --kill gpg-agent
53 |
--------------------------------------------------------------------------------
/ppg/__main__.py:
--------------------------------------------------------------------------------
1 | from logging import StreamHandler
2 | from textwrap import wrap
3 |
4 | import ppg.cmdline
5 | import logging
6 | import sys
7 |
8 | def _main():
9 | """
10 | Main entry point for the `ppg` command line script.
11 |
12 | We init logging here instead of in ppg.cmdline.main(...) because the latter
13 | can be called by projects using ppg, and it's bad practice for libraries to
14 | configure logging. See eg. https://stackoverflow.com/a/26087972/1839209.
15 | """
16 | _init_logging()
17 | ppg.cmdline.main()
18 |
19 | def _init_logging():
20 | # Redirect INFO or lower to stdout, WARNING or higher to stderr:
21 | stdout = _WrappingStreamHandler(sys.stdout)
22 | stdout.setLevel(logging.DEBUG)
23 | stdout.addFilter(lambda record: record.levelno <= logging.INFO)
24 | # Don't wrap stderr because it may contain stack traces:
25 | stderr = logging.StreamHandler(sys.stderr)
26 | stderr.setLevel(logging.WARNING)
27 | logging.basicConfig(
28 | level=logging.INFO, format='%(message)s', handlers=(stdout, stderr)
29 | )
30 |
31 | class _WrappingStreamHandler(StreamHandler):
32 | def __init__(self, stream=None, line_length=70):
33 | super().__init__(stream)
34 | self._line_length = line_length
35 | def format(self, record):
36 | result = super().format(record)
37 | if not getattr(record, 'wrap', True):
38 | # Make it possible to prevent wrapping. Eg.:
39 | # _LOG.info('Message', extra={'wrap': False})
40 | return result
41 | lines = result.split(self.terminator)
42 | new_lines = []
43 | for line in lines:
44 | new_lines.extend(
45 | wrap(line, self._line_length, replace_whitespace=False) or ['']
46 | )
47 | return self.terminator.join(new_lines)
48 |
49 | if __name__ == '__main__':
50 | _main()
--------------------------------------------------------------------------------
/ppg/_defaults/src/build/docker/ubuntu/Dockerfile:
--------------------------------------------------------------------------------
1 | # Build on an old Ubuntu version on purpose, to maximize compatibility:
2 | FROM ubuntu:16.04
3 |
4 | ARG requirements
5 |
6 | ARG python_version=3.6.12
7 | # List from https://github.com/pyenv/pyenv/wiki#suggested-build-environment:
8 | ARG python_build_deps="make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev"
9 |
10 | RUN apt-get update && \
11 | apt-get upgrade -y
12 |
13 | # Install pyenv:
14 | RUN apt-get install -y curl git
15 | ENV PYENV_ROOT /root/.pyenv
16 | ENV PATH $PYENV_ROOT/shims:$PYENV_ROOT/bin:$PATH
17 | RUN curl https://pyenv.run | bash
18 | RUN pyenv update
19 |
20 | # Install Python:
21 | RUN echo $python_build_deps | xargs apt-get install -y --no-install-recommends
22 | RUN CONFIGURE_OPTS=--enable-shared pyenv install $python_version && \
23 | pyenv global $python_version && \
24 | pyenv rehash
25 |
26 | # Add missing file libGL.so.1 for PyQt5.QtGui:
27 | RUN apt-get install libgl1-mesa-glx -y
28 |
29 | # fpm:
30 | RUN apt-get install ruby ruby-dev build-essential -y && \
31 | gem install --no-document fpm
32 |
33 | WORKDIR /root/${app_name}
34 |
35 | # Install Python requirements:
36 | ADD *.txt /tmp/requirements/
37 | RUN pip install --upgrade pip && \
38 | pip install -r "/tmp/requirements/${requirements}"
39 | RUN rm -rf /tmp/requirements/
40 |
41 | # Welcome message, displayed by ~/.bashrc:
42 | ADD motd /etc/motd
43 |
44 | ADD .bashrc /root/.bashrc
45 |
46 | # Requirements for our use of reprepro:
47 | ADD gpg-agent.conf gpg.conf /root/.gnupg/
48 | # Avoid GPG warning "unsafe permissions":
49 | RUN chmod -R 600 /root/.gnupg/
50 | RUN apt-get install reprepro gnupg-agent gnupg2 -y
51 | ADD private-key.gpg public-key.gpg /tmp/
52 | RUN gpg -q --batch --yes --passphrase ${gpg_pass} --import /tmp/private-key.gpg /tmp/public-key.gpg && \
53 | rm /tmp/private-key.gpg /tmp/public-key.gpg
--------------------------------------------------------------------------------
/ppg/_defaults/src/build/docker/fedora/Dockerfile:
--------------------------------------------------------------------------------
1 | # Build on an old Fedora version on purpose, to maximize compatibility:
2 | FROM fmanbuildsystem/fedora:25
3 |
4 | ARG requirements
5 |
6 | ARG python_version=3.6.12
7 | # List from https://github.com/pyenv/pyenv/wiki#suggested-build-environment:
8 | ARG python_build_deps="make gcc zlib-devel bzip2 bzip2-devel readline-devel sqlite sqlite-devel openssl-devel tk-devel libffi-devel xz-devel"
9 |
10 | RUN dnf -y update && dnf clean all
11 |
12 | # Install pyenv:
13 | RUN dnf install -y curl git
14 | ENV PYENV_ROOT /root/.pyenv
15 | ENV PATH $PYENV_ROOT/shims:$PYENV_ROOT/bin:$PATH
16 | RUN curl https://pyenv.run | bash
17 | RUN pyenv update
18 |
19 | # Install Python:
20 | # findutils contains xargs, which is needed for the next step:
21 | RUN dnf install -y findutils
22 | RUN echo $python_build_deps | xargs dnf install -y
23 | RUN CONFIGURE_OPTS=--enable-shared pyenv install $python_version && \
24 | pyenv global $python_version && \
25 | pyenv rehash
26 |
27 | # Install fpm:
28 | RUN dnf install -y ruby-devel gcc make rpm-build libffi-devel && \
29 | gem install --no-document fpm
30 |
31 | WORKDIR /root/${app_name}
32 |
33 | # SInstall Python requirements:
34 | ADD *.txt /tmp/requirements/
35 | RUN pip install --upgrade pip && \
36 | pip install -r "/tmp/requirements/${requirements}"
37 | RUN rm -rf /tmp/requirements/
38 |
39 | # Welcome message, displayed by ~/.bashrc:
40 | ADD motd /etc/motd
41 |
42 | ADD .bashrc /root/.bashrc
43 |
44 | ADD gpg-agent.conf /root/.gnupg/gpg-agent.conf
45 | # Avoid GPG warning "unsafe permissions":
46 | RUN chmod -R 600 /root/.gnupg
47 | ADD private-key.gpg public-key.gpg /tmp/
48 | RUN dnf install -y gpg rpm-sign && \
49 | gpg -q --batch --yes --passphrase ${gpg_pass} --import /tmp/private-key.gpg /tmp/public-key.gpg && \
50 | rpm --import /tmp/public-key.gpg && \
51 | rm /tmp/private-key.gpg /tmp/public-key.gpg
52 |
53 | ADD .rpmmacros /root
54 |
55 | RUN dnf install -y createrepo_c
56 |
57 | ENTRYPOINT ["/bin/bash"]
--------------------------------------------------------------------------------
/ppg/_defaults/src/freeze/windows/version_info.py:
--------------------------------------------------------------------------------
1 | # UTF-8
2 | #
3 | # For more details about fixed file info 'ffi' see:
4 | # http://msdn.microsoft.com/en-us/library/ms646997.aspx
5 |
6 | VSVersionInfo(
7 | ffi=FixedFileInfo(
8 | # filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4)
9 | # Set not needed items to zero 0. Must always contain 4 elements.
10 | filevers=(
11 | int('${version}'.split('.')[0]),
12 | int('${version}'.split('.')[1]),
13 | int('${version}'.split('.')[2]),
14 | 0
15 | ),
16 | prodvers=(
17 | int('${version}'.split('.')[0]),
18 | int('${version}'.split('.')[1]),
19 | int('${version}'.split('.')[2]),
20 | 0
21 | ),
22 | # Contains a bitmask that specifies the valid bits 'flags'r
23 | mask=0x3f,
24 | # Contains a bitmask that specifies the Boolean attributes of the file.
25 | flags=0x0,
26 | # The operating system for which this file was designed.
27 | # 0x4 - NT and there is no need to change it.
28 | OS=0x40004,
29 | # The general type of file.
30 | # 0x1 - the file is an application.
31 | fileType=0x1,
32 | # The function of the file.
33 | # 0x0 - the function is not defined for this fileType
34 | subtype=0x0,
35 | # Creation date and time stamp.
36 | date=(0, 0)
37 | ),
38 | kids=[
39 | StringFileInfo(
40 | [
41 | StringTable(
42 | '040904B0',
43 | [StringStruct('CompanyName', '${author}'),
44 | StringStruct('FileDescription', '${app_name}'),
45 | StringStruct('FileVersion', '${version}.0'),
46 | StringStruct('InternalName', '${app_name}'),
47 | StringStruct('LegalCopyright', '© ${author}. All rights reserved.'),
48 | StringStruct('OriginalFilename', '${app_name}.exe'),
49 | StringStruct('ProductName', '${app_name}'),
50 | StringStruct('ProductVersion', '${version}.0')])
51 | ]),
52 | VarFileInfo([VarStruct('Translation', [1033, 1200])])
53 | ]
54 | )
--------------------------------------------------------------------------------
/examples/first-app/src/main/python/main.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from ppg_runtime.application_context.PySide6 import ApplicationContext
3 | from ppg_runtime.application_context import PPGLifeCycle, Pydux, init_lifecycle
4 | from ppg_runtime.application_context.devtools.reloader import hot_reloading
5 | from ppg_runtime.application_context.utils import app_is_frozen
6 | from PySide6.QtWidgets import QMainWindow, QLabel
7 |
8 | # --------------------------------------------------------------------------------------
9 | # Important! Production Considerations for Hot Reloading
10 | # --------------------------------------------------------------------------------------
11 | # Hot reloading is a development tool that allows you to instantly see UI changes
12 | # when you save a file. It's extremely useful for rapid prototyping and designing
13 | # interfaces.
14 | #
15 | # However, this functionality is not designed for use in production environments.
16 | # For the final version of your application, it is highly recommended to remove
17 | # the code related to hot reloading, such as the `@hot_reloading` decorator
18 | # and the `window._init_hot_reload_system(__file__)` call.
19 | #
20 | # Keeping hot reloading active in production can negatively impact the application's
21 | # performance, stability, and security.
22 | # --------------------------------------------------------------------------------------
23 |
24 | @init_lifecycle
25 | @hot_reloading
26 | class Myfirstapp(QMainWindow, PPGLifeCycle, Pydux):
27 | def component_will_mount(self):
28 | self.subscribe_to_store(self)
29 |
30 | def render_(self):
31 | QLabel('Hello World!', parent=self)
32 |
33 | def responsive_UI(self):
34 | self.setMinimumSize(640, 480)
35 |
36 |
37 | if __name__ == '__main__':
38 | appctxt = ApplicationContext()
39 | window = Myfirstapp()
40 | if not app_is_frozen():
41 | window._init_hot_reload_system(__file__)
42 | window.show()
43 | exec_func = getattr(appctxt.app, 'exec', appctxt.app.exec_)
44 | sys.exit(exec_func())
--------------------------------------------------------------------------------
/ppg/builtin_commands/project_template/src/main/python/main.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from ppg_runtime.application_context.${python_bindings} import ApplicationContext
3 | from ppg_runtime.application_context import PPGLifeCycle, Pydux, init_lifecycle
4 | from ppg_runtime.application_context.devtools.reloader import hot_reloading
5 | from ppg_runtime.application_context.utils import app_is_frozen
6 | from ${python_bindings}.QtWidgets import QMainWindow, QLabel
7 |
8 | # --------------------------------------------------------------------------------------
9 | # Important! Production Considerations for Hot Reloading
10 | # --------------------------------------------------------------------------------------
11 | # Hot reloading is a development tool that allows you to instantly see UI changes
12 | # when you save a file. It's extremely useful for rapid prototyping and designing
13 | # interfaces.
14 | #
15 | # However, this functionality is not designed for use in production environments.
16 | # For the final version of your application, it is highly recommended to remove
17 | # the code related to hot reloading, such as the `@hot_reloading` decorator
18 | # and the `window._init_hot_reload_system(__file__)` call.
19 | #
20 | # Keeping hot reloading active in production can negatively impact the application's
21 | # performance, stability, and security.
22 | # --------------------------------------------------------------------------------------
23 |
24 | @init_lifecycle
25 | @hot_reloading
26 | class ${app_name}(QMainWindow, PPGLifeCycle, Pydux):
27 | def component_will_mount(self):
28 | self.subscribe_to_store(self)
29 |
30 | def render_(self):
31 | QLabel('Hello World!', parent=self)
32 |
33 | def responsive_UI(self):
34 | self.setMinimumSize(640, 480)
35 |
36 |
37 | if __name__ == '__main__':
38 | appctxt = ApplicationContext()
39 | window = ${app_name}()
40 | if not app_is_frozen():
41 | window._init_hot_reload_system(__file__)
42 | window.show()
43 | exec_func = getattr(appctxt.app, 'exec', appctxt.app.exec_)
44 | sys.exit(exec_func())
--------------------------------------------------------------------------------
/ppg_runtime/_settings.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | def load_settings(json_paths, base=None):
4 | """
5 | Return settings from the given JSON files as a dictionary. This function
6 | expands placeholders: That is, if a settings file contains
7 |
8 | {
9 | "app_name": "MyApp",
10 | "freeze_dir": "target/${app_name}"
11 | }
12 |
13 | then "freeze_dir" in the result of this function is "target/MyApp".
14 |
15 | It also merges lists. Say base.json contains
16 |
17 | { "hidden_imports": ["a"] }
18 |
19 | and mac.json contains
20 |
21 | { "hidden_imports": ["b"] }
22 |
23 | then you obtain
24 |
25 | { "hidden_imports": ["a", "b'] }.
26 | """
27 | if base is None:
28 | result = None
29 | else:
30 | result = dict(base)
31 | for json_path in json_paths:
32 | with open(json_path, 'r', encoding='utf-8') as f:
33 | data = json.load(f)
34 | result = data if result is None else _merge(result, data)
35 | while True:
36 | for key, value in result.items():
37 | new_value = expand_placeholders(value, result)
38 | if new_value != value:
39 | result[key] = new_value
40 | break
41 | else:
42 | break
43 | return result
44 |
45 | def expand_placeholders(obj, settings):
46 | if isinstance(obj, str):
47 | for key, value in settings.items():
48 | obj = obj.replace('${%s}' % key, str(value))
49 | elif isinstance(obj, list):
50 | return [expand_placeholders(o, settings) for o in obj]
51 | elif isinstance(obj, dict):
52 | return {k: expand_placeholders(v, settings) for k, v in obj.items()}
53 | return obj
54 |
55 | def _merge(a, b):
56 | if type(a) != type(b):
57 | raise ValueError('Cannot merge %r and %r' % (a, b))
58 | if isinstance(a, list):
59 | return a + b
60 | if isinstance(a, dict):
61 | result = dict(a)
62 | for k, v in b.items():
63 | result[k] = _merge(a[k], v) if k in a else v
64 | return result
65 | return b
--------------------------------------------------------------------------------
/ppg_runtime/_source.py:
--------------------------------------------------------------------------------
1 | """
2 | This module contains functions that should only be called by module `ppg`, or
3 | when running from source.
4 | """
5 |
6 | from ppg_runtime import FbsError
7 | from ppg_runtime._fbs import get_default_profiles, get_core_settings, \
8 | filter_public_settings
9 | from ppg_runtime._settings import load_settings
10 | from os.path import join, normpath, dirname, pardir, exists
11 | from pathlib import Path
12 |
13 | import os
14 |
15 | def get_project_dir():
16 | result = Path(os.getcwd())
17 | while result != result.parent:
18 | if (result / 'src' / 'main' / 'python').is_dir():
19 | return str(result)
20 | result = result.parent
21 | raise FbsError(
22 | 'Could not determine the project base directory. '
23 | 'Was expecting src/main/python.'
24 | )
25 |
26 | def get_resource_dirs(project_dir):
27 | result = [path(project_dir, 'src/main/icons')]
28 | resources = path(project_dir, 'src/main/resources')
29 | result.extend(
30 | join(resources, profile)
31 | # Resource dirs are listed most-specific first whereas profiles are
32 | # listed most-specific last. We therefore need to reverse the order:
33 | for profile in reversed(get_default_profiles())
34 | )
35 | return result
36 |
37 | def load_build_settings(project_dir):
38 | core_settings = get_core_settings(project_dir)
39 | profiles = get_default_profiles()
40 | json_paths = get_settings_paths(project_dir, profiles)
41 | all_settings = load_settings(json_paths, core_settings)
42 | return filter_public_settings(all_settings)
43 |
44 | def get_settings_paths(project_dir, profiles):
45 | return list(filter(exists, (
46 | path_fn('src/build/settings/%s.json' % profile)
47 | for path_fn in (default_path, lambda p: path(project_dir, p))
48 | for profile in profiles
49 | )))
50 |
51 | def default_path(path_str):
52 | defaults_dir = join(dirname(__file__), pardir, 'ppg', '_defaults')
53 | return path(defaults_dir, path_str)
54 |
55 | def path(base_dir, path_str):
56 | return normpath(join(base_dir, *path_str.split('/')))
--------------------------------------------------------------------------------
/ppg/installer/windows.py:
--------------------------------------------------------------------------------
1 | import re
2 | from pathlib import Path
3 | from ppg import path
4 | from ppg.installer import _generate_installer_resources
5 | from subprocess import check_call, DEVNULL
6 | from ppg._state import SETTINGS
7 |
8 | def create_installer_windows(user_level: bool = False):
9 | _generate_installer_resources()
10 |
11 | nsi_path = Path(path('target/installer/Installer.nsi'))
12 | app_name = SETTINGS['app_name']
13 |
14 | if not nsi_path.exists():
15 | raise FileNotFoundError(f"NSIS template not found: {nsi_path}")
16 |
17 | with open(nsi_path, encoding="utf-8") as f:
18 | text = f.read()
19 |
20 | if user_level:
21 | replacement = (
22 | "Function .onInit\n"
23 | " ; User-level installation\n"
24 | f" StrCpy $InstDir \"$LOCALAPPDATA\\\\{app_name}\"\n"
25 | "FunctionEnd"
26 | )
27 | else:
28 | # MultiUser Installation
29 | replacement = (
30 | "Function .onInit\n"
31 | " !insertmacro MULTIUSER_INIT\n"
32 | " ${If} $InstDir == \"\"\n"
33 | " ${If} $MultiUser.InstallMode == \"AllUsers\"\n"
34 | f" StrCpy $InstDir \"$PROGRAMFILES\\\\{app_name}\"\n"
35 | " ${Else}\n"
36 | f" StrCpy $InstDir \"$LOCALAPPDATA\\\\{app_name}\"\n"
37 | " ${EndIf}\n"
38 | " ${EndIf}\n"
39 | "FunctionEnd"
40 | )
41 | text = re.sub(
42 | r'Function \.onInit.*?FunctionEnd',
43 | replacement,
44 | text,
45 | flags=re.DOTALL
46 | )
47 |
48 | with open(nsi_path, "w", encoding="utf-8") as f:
49 | f.write(text)
50 |
51 | try:
52 | check_call(
53 | ['makensis', 'Installer.nsi'],
54 | cwd=path('target/installer'),
55 | stdout=DEVNULL
56 | )
57 | except FileNotFoundError:
58 | raise FileNotFoundError(
59 | "ppg could not find executable 'makensis'. Please install NSIS and "
60 | "add its installation directory to your PATH environment variable."
61 | ) from None
62 |
--------------------------------------------------------------------------------
/ppg/installer/mac/create-dmg/support/template.applescript:
--------------------------------------------------------------------------------
1 | on run (volumeName)
2 | tell application "Finder"
3 | tell disk (volumeName as string)
4 | open
5 |
6 | set theXOrigin to WINX
7 | set theYOrigin to WINY
8 | set theWidth to WINW
9 | set theHeight to WINH
10 |
11 | set theBottomRightX to (theXOrigin + theWidth)
12 | set theBottomRightY to (theYOrigin + theHeight)
13 | set dsStore to "\"" & "/Volumes/" & volumeName & "/" & ".DS_STORE\""
14 |
15 | tell container window
16 | set current view to icon view
17 | set toolbar visible to false
18 | set statusbar visible to false
19 | set the bounds to {theXOrigin, theYOrigin, theBottomRightX, theBottomRightY}
20 | set statusbar visible to false
21 | REPOSITION_HIDDEN_FILES_CLAUSE
22 | end tell
23 |
24 | set opts to the icon view options of container window
25 | tell opts
26 | set icon size to ICON_SIZE
27 | set text size to TEXT_SIZE
28 | set arrangement to not arranged
29 | end tell
30 | BACKGROUND_CLAUSE
31 |
32 | -- Positioning
33 | POSITION_CLAUSE
34 |
35 | -- Hiding
36 | HIDING_CLAUSE
37 |
38 | -- Application and QL Link Clauses
39 | APPLICATION_CLAUSE
40 | QL_CLAUSE
41 | close
42 | open
43 | -- Force saving of the size
44 | delay 1
45 |
46 | tell container window
47 | set statusbar visible to false
48 | set the bounds to {theXOrigin, theYOrigin, theBottomRightX - 10, theBottomRightY - 10}
49 | end tell
50 | end tell
51 |
52 | delay 1
53 |
54 | tell disk (volumeName as string)
55 | tell container window
56 | set statusbar visible to false
57 | set the bounds to {theXOrigin, theYOrigin, theBottomRightX, theBottomRightY}
58 | end tell
59 | end tell
60 |
61 | --give the finder some time to write the .DS_Store file
62 | delay 3
63 |
64 | set waitTime to 0
65 | set ejectMe to false
66 | repeat while ejectMe is false
67 | delay 1
68 | set waitTime to waitTime + 1
69 |
70 | if (do shell script "[ -f " & dsStore & " ]; echo $?") = "0" then set ejectMe to true
71 | end repeat
72 | log "waited " & waitTime & " seconds for .DS_STORE to be created."
73 | end tell
74 | end run
75 |
--------------------------------------------------------------------------------
/ppg/_gpg.py:
--------------------------------------------------------------------------------
1 | from ppg import SETTINGS
2 | from ppg_runtime import FbsError
3 | from subprocess import run, DEVNULL, check_call, check_output, PIPE, \
4 | CalledProcessError
5 |
6 | import re
7 |
8 | def preset_gpg_passphrase():
9 | # Ensure gpg-agent is running:
10 | run(
11 | ['gpg-agent', '--daemon', '--use-standard-socket', '-q'],
12 | stdout=DEVNULL, stderr=DEVNULL
13 | )
14 | gpg_key = SETTINGS['gpg_key']
15 | try:
16 | keygrip = _get_keygrip(gpg_key)
17 | except GpgDoesNotSupportKeygrip:
18 | # Old GPG versions don't support keygrips; They use the fingerprint
19 | # instead:
20 | keygrip = gpg_key
21 | check_call([
22 | SETTINGS['gpg_preset_passphrase'], '--preset', '--passphrase',
23 | SETTINGS['gpg_pass'], keygrip
24 | ], stdout=DEVNULL)
25 |
26 | def _get_keygrip(pubkey_id):
27 | try:
28 | output = check_output(
29 | ['gpg2', '--with-keygrip', '-K', pubkey_id],
30 | universal_newlines=True, stderr=PIPE
31 | )
32 | except CalledProcessError as e:
33 | if 'invalid option "--with-keygrip"' in e.stderr:
34 | raise GpgDoesNotSupportKeygrip() from None
35 | elif 'No secret key' in e.stderr:
36 | raise FbsError(
37 | "GPG could not read your key for code signing. Perhaps you "
38 | "don't want\nto run this command here, but after:\n"
39 | " ppg runvm {ubuntu|fedora|arch}"
40 | )
41 | raise
42 | pure_signing_subkey = _find_keygrip(output, 'S')
43 | if pure_signing_subkey:
44 | return pure_signing_subkey
45 | any_signing_key = _find_keygrip(output, '[^]]*S[^]]*')
46 | if any_signing_key:
47 | return any_signing_key
48 | raise RuntimeError('Keygrip not found. Output was:\n' + output)
49 |
50 | def _find_keygrip(gpg2_output, type_re):
51 | lines = gpg2_output.split('\n')
52 | for i, line in enumerate(lines):
53 | if re.match(r'.*\[%s\]$' % type_re, line):
54 | for keygrip_line in lines[i + 1:]:
55 | m = re.match(r' +Keygrip = ([A-Z0-9]{40})', keygrip_line)
56 | if m:
57 | return m.group(1)
58 |
59 | class GpgDoesNotSupportKeygrip(RuntimeError):
60 | pass
--------------------------------------------------------------------------------
/ppg/__init__.py:
--------------------------------------------------------------------------------
1 | from ppg import _state
2 | from ppg._state import LOADED_PROFILES
3 | from ppg_runtime import FbsError, _source
4 | from ppg_runtime._fbs import get_core_settings, get_default_profiles
5 | from ppg_runtime._settings import load_settings, expand_placeholders
6 | from ppg_runtime._source import get_settings_paths
7 | from os.path import abspath
8 |
9 | """
10 | ppg populates SETTINGS with the current build settings. A typical example is
11 | SETTINGS['app_name'], which you define in src/build/settings/base.json.
12 | """
13 | SETTINGS = _state.SETTINGS
14 |
15 | def init(project_dir):
16 | """
17 | Call this if you are invoking neither `ppg` on the command line nor
18 | ppg.cmdline.main() from Python.
19 | """
20 | SETTINGS.update(get_core_settings(abspath(project_dir)))
21 | for profile in get_default_profiles():
22 | activate_profile(profile)
23 |
24 | def activate_profile(profile_name):
25 | """
26 | By default, ppg only loads some settings. For instance,
27 | src/build/settings/base.json and .../`os`.json where `os` is one of "mac",
28 | "linux" or "windows". This function lets you load other settings on the fly.
29 | A common example would be during a release, where release.json contains the
30 | production server URL instead of a staging server.
31 | """
32 | LOADED_PROFILES.append(profile_name)
33 | project_dir = SETTINGS['project_dir']
34 | json_paths = get_settings_paths(project_dir, LOADED_PROFILES)
35 | core_settings = get_core_settings(project_dir)
36 | SETTINGS.update(load_settings(json_paths, core_settings))
37 |
38 | def path(path_str):
39 | """
40 | Return the absolute path of the given file in the project directory. For
41 | instance: path('src/main/python'). The `path_str` argument should always use
42 | forward slashes `/`, even on Windows. You can use placeholders to refer to
43 | settings. For example: path('${freeze_dir}/foo').
44 | """
45 | path_str = expand_placeholders(path_str, SETTINGS)
46 | try:
47 | project_dir = SETTINGS['project_dir']
48 | except KeyError:
49 | error_message = "Cannot call path(...) until ppg.init(...) has been " \
50 | "called."
51 | raise FbsError(error_message) from None
52 | return _source.path(project_dir, path_str)
--------------------------------------------------------------------------------
/ppg/builtin_commands/_licensing.py:
--------------------------------------------------------------------------------
1 | from ppg import path
2 | from ppg.builtin_commands import prompt_for_value, require_existing_project
3 | from ppg.builtin_commands._util import update_json, SECRET_JSON, BASE_JSON
4 | from ppg.cmdline import command
5 |
6 | import json
7 | import logging
8 |
9 | _LOG = logging.getLogger(__name__)
10 |
11 | @command
12 | def init_licensing():
13 | """
14 | Generate public/private keys for licensing
15 | """
16 | require_existing_project()
17 | try:
18 | import rsa
19 | except ImportError:
20 | _LOG.error(
21 | 'Please install Python library `rsa`. Eg. via:\n'
22 | ' pip install rsa'
23 | )
24 | return
25 | nbits = _prompt_for_nbits()
26 | print('')
27 | pubkey, privkey = rsa.newkeys(nbits)
28 | pubkey_args = {'n': pubkey.n, 'e': pubkey.e}
29 | privkey_args = {
30 | attr: getattr(privkey, attr) for attr in ('n', 'e', 'd', 'p', 'q')
31 | }
32 | update_json(path(SECRET_JSON), {
33 | 'licensing_privkey': privkey_args,
34 | 'licensing_pubkey': pubkey_args
35 | })
36 | try:
37 | with open(path(BASE_JSON)) as f:
38 | user_base_settings = json.load(f)
39 | except FileNotFoundError:
40 | user_base_settings = {}
41 | public_settings = user_base_settings.get('public_settings', [])
42 | if 'licensing_pubkey' not in public_settings:
43 | public_settings.append('licensing_pubkey')
44 | update_json(path(BASE_JSON), {'public_settings': public_settings})
45 | updated_base_json = True
46 | else:
47 | updated_base_json = False
48 | message = 'Saved a public/private key pair for licensing to:\n %s.\n' \
49 | % SECRET_JSON
50 | if updated_base_json:
51 | message += 'Also added "licensing_pubkey" to "public_settings" in' \
52 | '\n %s.\n' \
53 | '(This lets your app read the public key when it runs.)\n' \
54 | % BASE_JSON
55 | message += '\nFor details on how to implement licensing for your ' \
56 | 'application, see:\n '\
57 | ' https://build-system.fman.io/manual#licensing.'
58 | _LOG.info(message)
59 |
60 | def _prompt_for_nbits():
61 | while True:
62 | nbits_str = prompt_for_value('Bit size', default='2048')
63 | try:
64 | return int(nbits_str)
65 | except ValueError:
66 | continue
--------------------------------------------------------------------------------
/examples/multi-screens/src/main/python/main.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from ppg_runtime.application_context.PySide6 import ApplicationContext
3 | from ppg_runtime.application_context import PPGLifeCycle, Pydux, init_lifecycle
4 | from ppg_runtime.application_context.devtools.reloader import hot_reloading
5 | from ppg_runtime.application_context.utils import app_is_frozen
6 | from PySide6.QtWidgets import QMainWindow, QStackedWidget
7 | from views.Home import Home
8 | from views.AuthScreen import AuthScreen
9 |
10 | # --------------------------------------------------------------------------------------
11 | # Important! Production Considerations for Hot Reloading
12 | # --------------------------------------------------------------------------------------
13 | # Hot reloading is a development tool that allows you to instantly see UI changes
14 | # when you save a file. It's extremely useful for rapid prototyping and designing
15 | # interfaces.
16 | #
17 | # However, this functionality is not designed for use in production environments.
18 | # For the final version of your application, it is highly recommended to remove
19 | # the code related to hot reloading, such as the `@hot_reloading` decorator
20 | # and the `window._init_hot_reload_system(__file__)` call.
21 | #
22 | # Keeping hot reloading active in production can negatively impact the application's
23 | # performance, stability, and security.
24 | # --------------------------------------------------------------------------------------
25 |
26 | @init_lifecycle
27 | @hot_reloading
28 | class Multiscreensdemo(QMainWindow, PPGLifeCycle, Pydux):
29 | def component_will_mount(self):
30 | self.subscribe_to_store(self)
31 | self.set_schema({
32 | "current_screen_index": int
33 | })
34 |
35 | def render_(self):
36 | self.layout = QStackedWidget()
37 |
38 | for screen in (AuthScreen, Home):
39 | self.layout.addWidget(screen())
40 |
41 | self.setCentralWidget(self.layout)
42 |
43 | def responsive_UI(self):
44 | self.setMinimumSize(640, 480)
45 |
46 | def on_store_change(self, _):
47 | self.layout.setCurrentIndex(self.store['current_screen_index'])
48 |
49 |
50 | if __name__ == '__main__':
51 | appctxt = ApplicationContext()
52 | window = Multiscreensdemo()
53 | if not app_is_frozen():
54 | window._init_hot_reload_system(__file__)
55 | window.show()
56 | exec_func = getattr(appctxt.app, 'exec', appctxt.app.exec_)
57 | sys.exit(exec_func())
--------------------------------------------------------------------------------
/examples/reactive-components/src/main/python/main.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from ppg_runtime.application_context.PySide6 import ApplicationContext
3 | from ppg_runtime.application_context import PPGLifeCycle, Pydux, init_lifecycle
4 | from ppg_runtime.application_context.devtools.reloader import hot_reloading
5 | from ppg_runtime.application_context.utils import app_is_frozen
6 | from ppg_runtime.ReactiveWidgets import Label, TextEdit
7 | from PySide6.QtCore import Qt
8 | from PySide6.QtWidgets import QMainWindow
9 |
10 | # --------------------------------------------------------------------------------------
11 | # Important! Production Considerations for Hot Reloading
12 | # --------------------------------------------------------------------------------------
13 | # Hot reloading is a development tool that allows you to instantly see UI changes
14 | # when you save a file. It's extremely useful for rapid prototyping and designing
15 | # interfaces.
16 | #
17 | # However, this functionality is not designed for use in production environments.
18 | # For the final version of your application, it is highly recommended to remove
19 | # the code related to hot reloading, such as the `@hot_reloading` decorator
20 | # and the `window._init_hot_reload_system(__file__)` call.
21 | #
22 | # Keeping hot reloading active in production can negatively impact the application's
23 | # performance, stability, and security.
24 | # --------------------------------------------------------------------------------------
25 |
26 | @init_lifecycle
27 | @hot_reloading
28 | class Reactive(QMainWindow, PPGLifeCycle, Pydux):
29 | def component_will_mount(self):
30 | self.subscribe_to_store(self)
31 |
32 | # Define the schema for the store
33 | self.set_schema({
34 | "notes": str,
35 | })
36 |
37 | def render_(self):
38 | """
39 | This is an example of using Reactive Widgets with Pydux store,
40 | the widgets will automatically update when the store changes using the key provided
41 | """
42 | Label(self, key="notes", alignment=Qt.AlignCenter).move(20, 20)
43 | TextEdit(self, key="notes", placeholder="Type your notes here...").setGeometry(20, 120, 600, 300)
44 |
45 |
46 | def responsive_UI(self):
47 | self.setMinimumSize(640, 480)
48 |
49 |
50 | if __name__ == '__main__':
51 | appctxt = ApplicationContext()
52 | window = Reactive()
53 | if not app_is_frozen():
54 | window._init_hot_reload_system(__file__)
55 | window.show()
56 | exec_func = getattr(appctxt.app, 'exec', appctxt.app.exec_)
57 | sys.exit(exec_func())
--------------------------------------------------------------------------------
/ppg_runtime/_signal.py:
--------------------------------------------------------------------------------
1 | from socket import socketpair, SOCK_DGRAM
2 |
3 | import signal
4 |
5 | class SignalWakeupHandler:
6 | """
7 | Python's `signal` module lets us define custom signal handlers. What we want
8 | in particular is a graceful handling of Ctrl+C, meaning that the app shuts
9 | down cleanly.
10 |
11 | The default implementation in Python (ie. if we don't set any signals) is to
12 | raise KeyboardInterrupt at arbitrary points in our code. This is not nice or
13 | easy to handle.
14 |
15 | The simplest implementation would be to call `signal(SIGINT, SIG_DFL)`. This
16 | immediately kills the app when the user presses Ctrl in the console window
17 | attached to it. This is not nice because no shutdown/cleanup code is
18 | executed.
19 |
20 | Another implementation would be to call `signal(SIGINT, ...app.exit())`. The
21 | problem is: This signal seems to only be delivered when the app has focus.
22 | So the user presses Ctrl+C in the console, then needs to switch to the GUI
23 | window for the signal to be delivered and the app to shut down. Not nice
24 | either.
25 |
26 | The present class fixes this problem. It integrates Python and Qt so that
27 | signal handlers installed with Python get called immediately. This way, we
28 | can immediately quit the app when the user presses Ctrl+C in the console.
29 |
30 | See: https://stackoverflow.com/a/37229299/1839209
31 | """
32 | def __init__(self, app, QAbstractSocket):
33 | self._app = app
34 | self.old_fd = None
35 | # Create a socket pair
36 | self.wsock, self.rsock = socketpair(type=SOCK_DGRAM)
37 | self.socket = QAbstractSocket(QAbstractSocket.UdpSocket, app)
38 | # Let Qt listen on the one end
39 | self.socket.setSocketDescriptor(self.rsock.fileno())
40 | # And let Python write on the other end
41 | self.wsock.setblocking(False)
42 | self.old_fd = signal.set_wakeup_fd(self.wsock.fileno())
43 | # First Python code executed gets any exception from
44 | # the signal handler, so add a dummy handler first
45 | self.socket.readyRead.connect(lambda : None)
46 | # Second handler does the real handling
47 | self.socket.readyRead.connect(self._readSignal)
48 | def install(self):
49 | signal.signal(signal.SIGINT, lambda *_: self._app.exit(130))
50 | def __del__(self):
51 | # Restore any old handler on deletion
52 | if self.old_fd is not None and signal and signal.set_wakeup_fd:
53 | signal.set_wakeup_fd(self.old_fd)
54 | def _readSignal(self):
55 | # Read the written byte.
56 | # Note: readyRead is blocked from occurring again until readData()
57 | # was called, so call it, even if you don't need the value.
58 | _ = self.socket.readData(1)
--------------------------------------------------------------------------------
/ppg_runtime/platform.py:
--------------------------------------------------------------------------------
1 | from ppg_runtime import _state, FbsError
2 |
3 | import os
4 | import sys
5 |
6 | def is_windows():
7 | """
8 | Return True if the current OS is Windows, False otherwise.
9 | """
10 | return name() == 'Windows'
11 |
12 | def is_mac():
13 | """
14 | Return True if the current OS is macOS, False otherwise.
15 | """
16 | return name() == 'Mac'
17 |
18 | def is_linux():
19 | """
20 | Return True if the current OS is Linux, False otherwise.
21 | """
22 | return name() == 'Linux'
23 |
24 | def name():
25 | """
26 | Returns 'Windows', 'Mac' or 'Linux', depending on the current OS. If the OS
27 | can't be determined, FbsError is raised.
28 | """
29 | if _state.PLATFORM_NAME is None:
30 | _state.PLATFORM_NAME = _get_name()
31 | return _state.PLATFORM_NAME
32 |
33 | def _get_name():
34 | if sys.platform in ('win32', 'cygwin'):
35 | return 'Windows'
36 | if sys.platform == 'darwin':
37 | return 'Mac'
38 | if sys.platform.startswith('linux'):
39 | return 'Linux'
40 | raise FbsError('Unknown operating system.')
41 |
42 | def is_ubuntu():
43 | try:
44 | return linux_distribution() in ('Ubuntu', 'Linux Mint', 'Pop!_OS')
45 | except FileNotFoundError:
46 | return False
47 |
48 | def is_arch_linux():
49 | try:
50 | return linux_distribution() in ('Arch Linux', 'Manjaro Linux')
51 | except FileNotFoundError:
52 | return False
53 |
54 | def is_fedora():
55 | try:
56 | return linux_distribution() in ('Fedora', 'CentOS Linux')
57 | except FileNotFoundError:
58 | return False
59 |
60 | def linux_distribution():
61 | if _state.LINUX_DISTRIBUTION is None:
62 | _state.LINUX_DISTRIBUTION = _get_linux_distribution()
63 | return _state.LINUX_DISTRIBUTION
64 |
65 | def _get_linux_distribution():
66 | if not is_linux():
67 | return ''
68 | try:
69 | os_release = _get_os_release_name()
70 | except OSError:
71 | pass
72 | else:
73 | if os_release:
74 | return os_release
75 | return ''
76 |
77 | def is_gnome_based():
78 | curr_desktop = os.environ.get('XDG_CURRENT_DESKTOP', '').lower()
79 | return curr_desktop in ('unity', 'gnome', 'x-cinnamon')
80 |
81 | def is_kde_based():
82 | curr_desktop = os.environ.get('XDG_CURRENT_DESKTOP', '').lower()
83 | if curr_desktop == 'kde':
84 | return True
85 | gdmsession = os.environ.get('GDMSESSION', '').lower()
86 | return gdmsession.startswith('kde')
87 |
88 | def _get_os_release_name():
89 | with open('/etc/os-release', 'r', encoding='utf-8') as f:
90 | for line in f:
91 | line = line.rstrip()
92 | if line.startswith('NAME='):
93 | name = line[len('NAME='):]
94 | return name.strip('"')
95 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import os
2 | import json
3 | from os.path import relpath, join
4 | from pathlib import Path
5 | from setuptools import setup, find_packages
6 |
7 | def _load_package_json():
8 | with open("package.json", "r", encoding="utf-8") as file:
9 | return json.loads(file.read())
10 |
11 | def _get_package_data(pkg_dir, data_subdir):
12 | result = []
13 | for dirpath, _, filenames in os.walk(join(pkg_dir, data_subdir)):
14 | for filename in filenames:
15 | filepath = join(dirpath, filename)
16 | result.append(relpath(filepath, pkg_dir))
17 | return result
18 |
19 |
20 | PACKAGE = _load_package_json()
21 | long_description = (Path(__file__).parent / "README.md").read_text(encoding="utf-8")
22 |
23 | setup(
24 | name="ppg",
25 | version=PACKAGE["version"],
26 | description=PACKAGE["description"],
27 | long_description=long_description,
28 | long_description_content_type="text/markdown",
29 | author=PACKAGE["author"],
30 | author_email=PACKAGE["author_email"],
31 | url=PACKAGE["homepage"],
32 | packages=find_packages(exclude=("tests", "tests.*")),
33 | package_data={
34 | "ppg": _get_package_data("ppg", "_defaults"),
35 | "ppg.builtin_commands": (
36 | _get_package_data("ppg/builtin_commands", "project_template")
37 | + ["package.json"]
38 | ),
39 | "ppg.builtin_commands._gpg": ["Dockerfile", "genkey.sh", "gpg-agent.conf"],
40 | "ppg.installer.mac": _get_package_data("ppg/installer/mac", "create-dmg"),
41 | },
42 | install_requires=["PyInstaller>=6.9.0", "pydantic>=2.11.7",
43 | "questionary==2.1.0", "rich==14.1.0", "watchdog==6.0.0", "astor==0.8.1", "prompt_toolkit==3.0.39"],
44 | extras_require={
45 | "licensing": ["rsa>=3.4.2"],
46 | "sentry": ["sentry-sdk>=0.6.6"],
47 | "upload": ["boto3"]
48 | },
49 | classifiers=[
50 | "Intended Audience :: Developers",
51 | "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
52 | "Operating System :: OS Independent",
53 | "Topic :: Software Development :: Libraries",
54 | "Topic :: Software Development :: Libraries :: Python Modules"
55 |
56 | "Programming Language :: Python :: 3.8",
57 | "Programming Language :: Python :: 3.9",
58 | "Programming Language :: Python :: 3.10",
59 | "Programming Language :: Python :: 3.11",
60 | "Programming Language :: Python :: 3.12",
61 | ],
62 | entry_points={
63 | "console_scripts": ["ppg=ppg.__main__:_main"]
64 | },
65 | license=PACKAGE["license"],
66 | keywords=PACKAGE["keywords"],
67 | platforms=["MacOS", "Windows", "Debian",
68 | "Fedora", "CentOS", "Arch", "Raspbian"],
69 | test_suite="tests",
70 | data_files=[
71 | ("ppg/builtin_commands", ["package.json"]),
72 | ],
73 | )
74 |
--------------------------------------------------------------------------------
/ppg/installer/mac/create-dmg/support/eula-resources-template.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | LPic
6 |
7 |
8 | Attributes
9 | 0x0000
10 | Data
11 |
12 | AAAAAgAAAAAAAAAAAAQAAA==
13 |
14 | ID
15 | 5000
16 | Name
17 |
18 |
19 |
20 | STR#
21 |
22 |
23 | Attributes
24 | 0x0000
25 | Data
26 |
27 | AAYNRW5nbGlzaCB0ZXN0MQVBZ3JlZQhEaXNhZ3JlZQVQcmludAdT
28 | YXZlLi4ueklmIHlvdSBhZ3JlZSB3aXRoIHRoZSB0ZXJtcyBvZiB0
29 | aGlzIGxpY2Vuc2UsIGNsaWNrICJBZ3JlZSIgdG8gYWNjZXNzIHRo
30 | ZSBzb2Z0d2FyZS4gSWYgeW91IGRvIG5vdCBhZ3JlZSwgY2xpY2sg
31 | IkRpc2FncmVlIi4=
32 |
33 | ID
34 | 5000
35 | Name
36 | English buttons
37 |
38 |
39 | Attributes
40 | 0x0000
41 | Data
42 |
43 | AAYHRW5nbGlzaAVBZ3JlZQhEaXNhZ3JlZQVQcmludAdTYXZlLi4u
44 | e0lmIHlvdSBhZ3JlZSB3aXRoIHRoZSB0ZXJtcyBvZiB0aGlzIGxp
45 | Y2Vuc2UsIHByZXNzICJBZ3JlZSIgdG8gaW5zdGFsbCB0aGUgc29m
46 | dHdhcmUuIElmIHlvdSBkbyBub3QgYWdyZWUsIGNsaWNrICJEaXNh
47 | Z3JlZSIu
48 |
49 | ID
50 | 5002
51 | Name
52 | English
53 |
54 |
55 | ${EULA_FORMAT}
56 |
57 |
58 | Attributes
59 | 0x0000
60 | Data
61 |
62 | ${EULA_DATA}
63 |
64 | ID
65 | 5000
66 | Name
67 | English
68 |
69 |
70 | TMPL
71 |
72 |
73 | Attributes
74 | 0x0000
75 | Data
76 |
77 | E0RlZmF1bHQgTGFuZ3VhZ2UgSUREV1JEBUNvdW50T0NOVAQqKioq
78 | TFNUQwtzeXMgbGFuZyBJRERXUkQebG9jYWwgcmVzIElEIChvZmZz
79 | ZXQgZnJvbSA1MDAwRFdSRBAyLWJ5dGUgbGFuZ3VhZ2U/RFdSRAQq
80 | KioqTFNURQ==
81 |
82 | ID
83 | 128
84 | Name
85 | LPic
86 |
87 |
88 | styl
89 |
90 |
91 | Attributes
92 | 0x0000
93 | Data
94 |
95 | AAMAAAAAAAwACQAUAAAAAAAAAAAAAAAAACcADAAJABQBAAAAAAAA
96 | AAAAAAAAKgAMAAkAFAAAAAAAAAAAAAA=
97 |
98 | ID
99 | 5000
100 | Name
101 | English
102 |
103 |
104 |
105 |
106 |
--------------------------------------------------------------------------------
/ppg/builtin_commands/_util.py:
--------------------------------------------------------------------------------
1 | from collections import OrderedDict
2 | from ppg import path
3 | from ppg_runtime import FbsError
4 | from getpass import getpass
5 | from os.path import exists
6 | from pathlib import Path
7 |
8 | import json
9 | import re
10 |
11 | BASE_JSON = 'src/build/settings/base.json'
12 | SECRET_JSON = 'src/build/settings/secret.json'
13 |
14 | def prompt_for_value(
15 | value, optional=False, default='', password=False, choices=()
16 | ):
17 | message = value
18 | if choices:
19 | choices_dict = \
20 | OrderedDict((str(i + 1), c) for (i, c) in enumerate(choices))
21 | message += ': '
22 | message += ' or '.join('%s) %s' % tpl for tpl in choices_dict.items())
23 | if default:
24 | message += ' [%s] ' % \
25 | (choices.index(default) + 1 if choices else default)
26 | message += ': '
27 | prompt = getpass if password else input
28 | result = prompt(message).strip()
29 | if not result and default:
30 | print(default)
31 | return default
32 | if not optional:
33 | while not result or (choices and result not in choices_dict):
34 | result = prompt(message).strip()
35 | return choices_dict[result] if choices else result
36 |
37 | def require_existing_project():
38 | if not exists(path('src')):
39 | raise FbsError(
40 | "Could not find the src/ directory. Are you in the right folder?\n"
41 | "If yes, did you already run\n"
42 | " ppg startproject ?"
43 | )
44 |
45 | def require_frozen_app():
46 | if not exists(path('${freeze_dir}')):
47 | raise FbsError(
48 | 'It seems your app has not yet been frozen. Please run:\n'
49 | ' ppg freeze'
50 | )
51 |
52 | def require_installer():
53 | installer = path('target/${installer}')
54 | if not exists(installer):
55 | raise FbsError(
56 | 'Installer does not exist. Maybe you need to run:\n'
57 | ' ppg installer'
58 | )
59 |
60 | def update_json(f_path, dict_):
61 | f = Path(f_path)
62 | try:
63 | contents = f.read_text()
64 | except FileNotFoundError:
65 | indent = _infer_indent(Path(path(BASE_JSON)).read_text())
66 | new_contents = json.dumps(dict_, indent=indent)
67 | else:
68 | new_contents = _update_json_str(contents, dict_)
69 | f.write_text(new_contents)
70 |
71 | def is_valid_version(version_str):
72 | return bool(re.match(r'\d+\.\d+\.\d+$', version_str))
73 |
74 | def _update_json_str(json_str, dict_):
75 | if not dict_:
76 | return json_str
77 | data = json.loads(json_str, object_pairs_hook=OrderedDict)
78 | data.update(dict_)
79 | indent = _infer_indent(json_str)
80 | return json.dumps(data, indent=indent)
81 |
82 | def _infer_indent(json_str):
83 | start = json_str.find('{')
84 | if start == -1:
85 | return None
86 | match = re.search('\n(\\s+)', json_str[start:])
87 | return match.group(1) if match else None
--------------------------------------------------------------------------------
/ppg_runtime/ReactiveWidgets/label.py:
--------------------------------------------------------------------------------
1 | try:
2 | from PySide6.QtWidgets import QLabel
3 | except ImportError:
4 | try:
5 | from PySide2.QtWidgets import QLabel
6 | except ImportError:
7 | try:
8 | from PyQt6.QtWidgets import QLabel
9 | except ImportError:
10 | try:
11 | from PyQt5.QtWidgets import QLabel
12 | except ImportError:
13 | raise ImportError(
14 | "No Qt bindings found. Install PySide6, PySide2, PyQt6 or PyQt5."
15 | )
16 |
17 |
18 | class ReactiveLabel(QLabel):
19 | """
20 | Reactive QLabel connected to Pydux, including support for nested models.
21 | """
22 |
23 | def __init__(self, parent, key: str = "", placeholder: str = "", text: str = "", onChange=None, **kwargs):
24 | super().__init__(parent, **kwargs)
25 | self._store_key = key
26 | self._parent = parent
27 | self._placeholder = placeholder
28 | self._template_text = text
29 | self._onChange = onChange
30 |
31 | # Subscribe to store changes if parent supports it
32 | if hasattr(parent, "subscribe_to_store"):
33 | parent.subscribe_to_store(self)
34 |
35 | # Initialize label text
36 | self._update_from_store()
37 |
38 | def _update_from_store(self):
39 | """
40 | Updates the QLabel text from the store.
41 | """
42 | if self._template_text:
43 | # Get the entire store to use for formatting
44 | if hasattr(self._parent, "store"):
45 | store_data = self._parent.store
46 | else:
47 | # Fallback if no store is found
48 | store_data = {}
49 |
50 | try:
51 | # Format the text with all values from the store
52 | current_value = self._template_text.format(**store_data)
53 | except KeyError:
54 | # If a key in the template doesn't exist in the store
55 | # This prevents the app from crashing.
56 | current_value = self._template_text
57 | else:
58 | # Original logic for non-templated text
59 | if hasattr(self._parent, "get_nested"):
60 | current_value = self._parent.get_nested(self._store_key) or ""
61 | else:
62 | current_value = str(
63 | self._parent.store.get(self._store_key, ""))
64 |
65 | # Use placeholder if value is empty
66 | if not current_value and self._placeholder:
67 | current_value = self._placeholder
68 |
69 | # Update text if it's different
70 | if self.text() != str(current_value):
71 | self.setText(str(current_value))
72 | self.adjustSize()
73 | if callable(self._onChange):
74 | self._onChange(current_value)
75 |
76 | def on_store_change(self, store):
77 | """
78 | Method automatically called when the store changes.
79 | """
80 | self._update_from_store()
81 |
--------------------------------------------------------------------------------
/ppg_runtime/ReactiveWidgets/check_box.py:
--------------------------------------------------------------------------------
1 | try:
2 | from PySide6.QtWidgets import QCheckBox
3 | except ImportError:
4 | try:
5 | from PySide2.QtWidgets import QCheckBox
6 | except ImportError:
7 | try:
8 | from PyQt6.QtWidgets import QCheckBox
9 | except ImportError:
10 | try:
11 | from PyQt5.QtWidgets import QCheckBox
12 | except ImportError:
13 | raise ImportError("No Qt bindings found.")
14 |
15 | from typing import Callable, Optional
16 |
17 | class ReactiveCheckBox(QCheckBox):
18 | """
19 | Reactive QCheckBox connected to Pydux, including support for nested models.
20 |
21 | Args:
22 | parent: Parent widget that contains the store.
23 | key: Key in the store (can be nested, e.g., "user.active").
24 | text: Optional text for the checkbox label.
25 | onChange: Optional callback when the checkbox state changes.
26 | """
27 | def __init__(self, parent, key: str, text: str = "", onChange: Optional[Callable[[bool], None]] = None, **kwargs):
28 | super().__init__(text, parent, **kwargs)
29 | self._store_key = key
30 | self._parent = parent
31 | self._updating_from_store = False
32 | self._onChange = onChange
33 |
34 | # Initialize checkbox state from store
35 | value = self._parent.get_nested(self._store_key) or False
36 | self.setChecked(bool(value))
37 |
38 | # Connect state change signal
39 | self.stateChanged.connect(self._on_state_changed)
40 |
41 | # Subscribe to store changes if parent supports it
42 | if hasattr(parent, "subscribe_to_store"):
43 | parent.subscribe_to_store(self)
44 |
45 | def _on_state_changed(self, state):
46 | """
47 | Internal callback when checkbox state changes.
48 | Updates the store (supports nested keys).
49 | """
50 | if self._updating_from_store:
51 | return
52 |
53 | if "." in self._store_key:
54 | model_key, nested_field = self._store_key.split(".", 1)
55 | if hasattr(self._parent, "update_nested_model"):
56 | self._parent.update_nested_model(model_key, {nested_field: bool(state)})
57 | else:
58 | self._parent.update_store({self._store_key: bool(state)})
59 |
60 | if callable(self._onChange):
61 | self._onChange(state)
62 |
63 | def _update_from_store(self):
64 | """
65 | Updates the checkbox state from the store.
66 | Supports nested keys using get_nested().
67 | """
68 | value = self._parent.get_nested(self._store_key) or False
69 | if self.isChecked() != bool(value):
70 | self._updating_from_store = True
71 | self.setChecked(bool(value))
72 | self._updating_from_store = False
73 |
74 | def on_store_change(self, store):
75 | """
76 | Method automatically called when the store changes.
77 | Refreshes the checkbox state from the store.
78 | """
79 | self._update_from_store()
80 |
--------------------------------------------------------------------------------
/ppg_runtime/ReactiveWidgets/text_edit.py:
--------------------------------------------------------------------------------
1 | try:
2 | from PySide6.QtWidgets import QTextEdit
3 | except ImportError:
4 | try:
5 | from PySide2.QtWidgets import QTextEdit
6 | except ImportError:
7 | try:
8 | from PyQt6.QtWidgets import QTextEdit
9 | except ImportError:
10 | try:
11 | from PyQt5.QtWidgets import QTextEdit
12 | except ImportError:
13 | raise ImportError("No Qt bindings found.")
14 |
15 |
16 | class ReactiveTextEdit(QTextEdit):
17 | """
18 | Reactive QTextEdit connected to Pydux, including support for nested models.
19 |
20 | Args:
21 | parent: Parent widget that contains the store.
22 | key: Key in the store (can be nested, e.g., "user.description").
23 | placeholder: Optional placeholder text when value is empty.
24 | onChange: Optional callback when the text changes.
25 | """
26 | def __init__(self, parent, key, placeholder: str = "", onChange=None, **kwargs):
27 | super().__init__(parent, **kwargs)
28 | self._store_key = key
29 | self._parent = parent
30 | self._updating_from_store = False
31 | self._onChange = onChange
32 |
33 | # Initialize text from store (supports nested)
34 | self._update_from_store()
35 |
36 | if placeholder:
37 | self.setPlaceholderText(placeholder)
38 |
39 | # Connect text changed signal
40 | self.textChanged.connect(self._on_text_changed)
41 |
42 | # Subscribe to store changes
43 | if hasattr(parent, "subscribe_to_store"):
44 | parent.subscribe_to_store(self)
45 |
46 | def _on_text_changed(self):
47 | """
48 | Internal callback when the text changes.
49 | Updates the store (supports nested keys).
50 | """
51 | if self._updating_from_store:
52 | return
53 |
54 | text = self.toPlainText()
55 | if "." in self._store_key and hasattr(self._parent, "update_nested_model"):
56 | model_key, nested_field = self._store_key.split(".", 1)
57 | self._parent.update_nested_model(model_key, {nested_field: text})
58 | else:
59 | self._parent.update_store({self._store_key: text})
60 |
61 | if callable(self._onChange):
62 | self._onChange(text)
63 |
64 | def _update_from_store(self):
65 | """
66 | Syncs the QTextEdit content with the store.
67 | Supports nested keys using get_nested().
68 | """
69 | if hasattr(self._parent, "get_nested"):
70 | value = self._parent.get_nested(self._store_key) or ""
71 | else:
72 | value = str(self._parent.store.get(self._store_key, ""))
73 |
74 | if self.toPlainText() != str(value):
75 | self._updating_from_store = True
76 | self.setPlainText(str(value))
77 | self._updating_from_store = False
78 |
79 | def on_store_change(self, store):
80 | """
81 | Method automatically called when the store changes.
82 | Simply refreshes the QTextEdit content from the store.
83 | """
84 | self._update_from_store()
85 |
--------------------------------------------------------------------------------
/ppg/builtin_commands/_gpg/__init__.py:
--------------------------------------------------------------------------------
1 | from ppg import path, SETTINGS
2 | from ppg.builtin_commands._docker import _run_docker
3 | from ppg.builtin_commands._util import prompt_for_value, update_json, \
4 | require_existing_project, BASE_JSON, SECRET_JSON
5 | from ppg.cmdline import command
6 | from ppg_runtime import FbsError
7 | from os import makedirs
8 | from os.path import dirname, exists
9 | from pathlib import Path
10 | from subprocess import DEVNULL, PIPE
11 |
12 | import logging
13 |
14 | _LOG = logging.getLogger(__name__)
15 | _DOCKER_IMAGE = 'fbs/gpg-generator'
16 | _DEST_DIR = 'src/sign/linux'
17 | _PUBKEY_NAME = 'public-key.gpg'
18 | _PRIVKEY_NAME = 'private-key.gpg'
19 |
20 | @command
21 | def gengpgkey():
22 | """
23 | Generate a GPG key for Linux code signing
24 | """
25 | require_existing_project()
26 | if exists(_DEST_DIR):
27 | raise FbsError('The %s folder already exists. Aborting.' % _DEST_DIR)
28 | try:
29 | email = prompt_for_value('Email address')
30 | name = prompt_for_value('Real name', default=SETTINGS['author'])
31 | passphrase = prompt_for_value('Key password', password=True)
32 | except KeyboardInterrupt:
33 | print('')
34 | return
35 | print('')
36 | _LOG.info('Generating the GPG key. This can take a little...')
37 | _init_docker()
38 | args = ['run', '-t']
39 | if exists('/dev/urandom'):
40 | # Give the key generator more entropy on Posix:
41 | args.extend(['-v', '/dev/urandom:/dev/random'])
42 | args.extend([_DOCKER_IMAGE, '/root/genkey.sh', name, email, passphrase])
43 | result = _run_docker(args, check=True, stdout=PIPE, universal_newlines=True)
44 | key = _snip(
45 | result.stdout,
46 | "revocation certificate stored as '/root/.gnupg/openpgp-revocs.d/",
47 | ".rev'",
48 | include_bounds=False
49 | )
50 | pubkey = _snip(result.stdout,
51 | '-----BEGIN PGP PUBLIC KEY BLOCK-----\n',
52 | '-----END PGP PUBLIC KEY BLOCK-----\n')
53 | privkey = _snip(result.stdout,
54 | '-----BEGIN PGP PRIVATE KEY BLOCK-----\n',
55 | '-----END PGP PRIVATE KEY BLOCK-----\n')
56 | makedirs(path(_DEST_DIR), exist_ok=True)
57 | pubkey_dest = _DEST_DIR + '/' + _PUBKEY_NAME
58 | Path(path(pubkey_dest)).write_text(pubkey)
59 | Path(path(_DEST_DIR + '/' + _PRIVKEY_NAME)).write_text(privkey)
60 | update_json(path(BASE_JSON), {'gpg_key': key, 'gpg_name': name})
61 | update_json(path(SECRET_JSON), {'gpg_pass': passphrase})
62 | _LOG.info(
63 | 'Done. Created %s and ...%s. Also updated %s and ...secret.json with '
64 | 'the values you provided.', pubkey_dest, _PRIVKEY_NAME, BASE_JSON
65 | )
66 |
67 | def _init_docker():
68 | _run_docker(
69 | ['build', '-t', _DOCKER_IMAGE, dirname(__file__)], stdout=DEVNULL
70 | )
71 |
72 | def _snip(str_, preamble, postamble, include_bounds=True):
73 | start = str_.index(preamble)
74 | end = str_.index(postamble, start + len(preamble))
75 | if not include_bounds:
76 | start += len(preamble)
77 | if include_bounds:
78 | end += len(postamble)
79 | return str_[start:end]
--------------------------------------------------------------------------------
/ppg_runtime/ReactiveWidgets/line_edit.py:
--------------------------------------------------------------------------------
1 | try:
2 | from PySide6.QtWidgets import QLineEdit
3 | except ImportError:
4 | try:
5 | from PySide2.QtWidgets import QLineEdit
6 | except ImportError:
7 | try:
8 | from PyQt6.QtWidgets import QLineEdit
9 | except ImportError:
10 | try:
11 | from PyQt5.QtWidgets import QLineEdit
12 | except ImportError:
13 | raise ImportError(
14 | "No Qt bindings found. Install PySide6, PySide2, PyQt6 or PyQt5."
15 | )
16 | class ReactiveLineEdit(QLineEdit):
17 | """
18 | Reactive QLineEdit that connects to Pydux, including support for nested models.
19 |
20 | Args:
21 | parent: Parent widget that contains the store.
22 | key: Key in the store (can be nested, e.g. "user.name").
23 | placeholder: Optional placeholder text.
24 | onChange: Optional callback when the text changes.
25 |
26 | """
27 | def __init__(self, parent, key, placeholder: str = "", onChange=None, **kwargs):
28 | super().__init__(parent, **kwargs)
29 | self._store_key = key
30 | self._parent = parent
31 | self._updating_from_store = False
32 | self._onChange = onChange
33 |
34 | # Initialize text from the store (supports nested keys)
35 | self._update_from_store()
36 |
37 | if placeholder:
38 | self.setPlaceholderText(placeholder)
39 |
40 | # Connect text change signal
41 | self.textChanged.connect(self._on_text_changed)
42 |
43 | # Subscribe to store changes if the parent allows it
44 | if hasattr(parent, "subscribe_to_store"):
45 | parent.subscribe_to_store(self)
46 |
47 | def _on_text_changed(self, text):
48 | """
49 | Internal callback when the LineEdit text changes.
50 | Updates the store (supports nested keys).
51 | """
52 | if self._updating_from_store:
53 | return
54 |
55 | # Check for nested key
56 | if "." in self._store_key:
57 | model_key, nested_field = self._store_key.split(".", 1)
58 | if hasattr(self._parent, "update_nested_model"):
59 | self._parent.update_nested_model(model_key, {nested_field: text})
60 | else:
61 | self._parent.update_store({self._store_key: text})
62 |
63 | if callable(self._onChange):
64 | self._onChange(text)
65 |
66 | def _update_from_store(self):
67 | """
68 | Updates the QLineEdit text from the store.
69 | Supports nested keys with get_nested().
70 | """
71 | value = ""
72 | if hasattr(self._parent, "get_nested"):
73 | value = self._parent.get_nested(self._store_key) or ""
74 | if self.text() != str(value):
75 | self._updating_from_store = True
76 | self.setText(str(value))
77 | self._updating_from_store = False
78 |
79 | def on_store_change(self, store):
80 | """
81 | Method that is called automatically when the store changes.
82 | Here we simply refresh the value from the store.
83 | """
84 | self._update_from_store()
85 |
--------------------------------------------------------------------------------
/ppg_runtime/ReactiveWidgets/spin_box.py:
--------------------------------------------------------------------------------
1 | try:
2 | from PySide6.QtWidgets import QSpinBox
3 | except ImportError:
4 | try:
5 | from PySide2.QtWidgets import QSpinBox
6 | except ImportError:
7 | try:
8 | from PyQt6.QtWidgets import QSpinBox
9 | except ImportError:
10 | try:
11 | from PyQt5.QtWidgets import QSpinBox
12 | except ImportError:
13 | raise ImportError("No Qt bindings found.")
14 |
15 |
16 | class ReactiveSpinBox(QSpinBox):
17 | """
18 | Reactive QSpinBox connected to Pydux, including support for nested models.
19 |
20 | Args:
21 | parent: Parent widget that contains the store.
22 | key: Key in the store (can be nested, e.g., "settings.count").
23 | onChange: Optional callback when value changes.
24 | minimum: Minimum value (default 0).
25 | maximum: Maximum value (default 100).
26 | """
27 | def __init__(self, parent, key, onChange=None, minimum=0, maximum=100, **kwargs):
28 | super().__init__(parent, **kwargs)
29 | self._store_key = key
30 | self._parent = parent
31 | self._updating_from_store = False
32 | self._onChange = onChange
33 |
34 | self.setRange(minimum, maximum)
35 |
36 | # Initialize value from store (supports nested)
37 | if hasattr(parent, "get_nested"):
38 | value = parent.get_nested(key) or minimum
39 | else:
40 | value = parent.store.get(key, minimum)
41 | self.setValue(int(value))
42 |
43 | # Connect signals
44 | self.valueChanged.connect(self._on_value_changed)
45 |
46 | # Subscribe to store changes
47 | if hasattr(parent, "subscribe_to_store"):
48 | parent.subscribe_to_store(self)
49 |
50 | def _on_value_changed(self, value):
51 | """
52 | Internal callback when the spinbox value changes.
53 | Updates the store (supports nested).
54 | """
55 | if self._updating_from_store:
56 | return
57 |
58 | if "." in self._store_key and hasattr(self._parent, "update_nested_model"):
59 | model_key, nested_field = self._store_key.split(".", 1)
60 | self._parent.update_nested_model(model_key, {nested_field: value})
61 | else:
62 | self._parent.update_store({self._store_key: value})
63 |
64 | if callable(self._onChange):
65 | self._onChange(value)
66 |
67 | def _update_from_store(self):
68 | """
69 | Syncs the spinbox value with the store.
70 | Supports nested keys using get_nested().
71 | """
72 | if hasattr(self._parent, "get_nested"):
73 | value = self._parent.get_nested(self._store_key) or self.minimum()
74 | else:
75 | value = self._parent.store.get(self._store_key, self.minimum())
76 |
77 | value = int(value)
78 | if self.value() != value:
79 | self._updating_from_store = True
80 | self.setValue(value)
81 | self._updating_from_store = False
82 |
83 | def on_store_change(self, store):
84 | """
85 | Method automatically called when the store changes.
86 | Simply refreshes the spinbox value from the store.
87 | """
88 | self._update_from_store()
89 |
--------------------------------------------------------------------------------
/examples/hybryd-app/src/main/resources/base/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | My Demo example
7 |
8 |
9 |
10 |
11 |
12 | Hello world!
13 | This is a simple example of a hybrid application using PPG.
14 |
15 |
16 |
25 |
26 |
27 |
28 |
29 |
30 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/ppg_runtime/licensing.py:
--------------------------------------------------------------------------------
1 | from base64 import b64encode, b64decode
2 | from rsa import PrivateKey, PublicKey, VerificationError
3 |
4 | import json
5 | import rsa
6 |
7 | def pack_license_key(data, privkey_args):
8 | """
9 | Pack a dictionary of license key data to a string. You typically call this
10 | function on a server, when a user purchases a license. Eg.:
11 |
12 | lk_contents = pack_license_key({'email': 'some@user.com'}, ...)
13 |
14 | The parameter `privkey_args` is a dictionary containing values for the RSA
15 | fields "n", "e", "d", "p" and "q". You can generate it with ppg's command
16 | `init_licensing`.
17 |
18 | The resulting string is signed to prevent the end user from changing it.
19 | Use the function `unpack_license_key` below to reconstruct `data` from it.
20 | This also verifies that the string was not tampered with.
21 |
22 | This function has two non-obvious caveats:
23 |
24 | 1) It does not obfuscate the data. If `data` contains "key": "value", then
25 | "key": "value" is also visible in the resulting string.
26 |
27 | 2) Calling this function twice with the same arguments will result in the
28 | same string. This may be undesirable when you generate multiple license keys
29 | for the same user. A simple workaround for this is to add a unique parameter
30 | to `data`, such as the current timestamp.
31 | """
32 | data_bytes = _dumpb(data)
33 | signature = rsa.sign(data_bytes, PrivateKey(**privkey_args), 'SHA-1')
34 | result = dict(data)
35 | if 'key' in data:
36 | raise ValueError('Data must not contain an element called "key"')
37 | result['key'] = b64encode(signature).decode('ascii')
38 | return json.dumps(result)
39 |
40 | class _Licensing:
41 | """
42 | This internal class lets us inject the licensing functionality into the
43 | application context, in such a way that the ppg user does not have to worry
44 | about where the public key is stored.
45 | """
46 | def __init__(self, pubkey_args):
47 | self._pubkey = pubkey_args
48 | def unpack_license_key(self, key_str):
49 | return unpack_license_key(key_str, self._pubkey)
50 |
51 | class InvalidKey(Exception):
52 | pass
53 |
54 | def unpack_license_key(key_str, pubkey_args):
55 | """
56 | Decode a string of license key data produced by `pack_license_key`. In other
57 | words, this function is the inverse of `pack_...` above:
58 |
59 | data == unpack_license_key(pack_license_key(data, ...), ...)
60 |
61 | If the given string is not a valid key, `InvalidKey` is raised.
62 |
63 | The parameter `pubkey_args` is a dictionary containing values for the RSA
64 | fields "n" and "e". It can be generated with ppg's command `init_licensing`.
65 | """
66 | try:
67 | result = json.loads(key_str)
68 | except ValueError:
69 | raise InvalidKey() from None
70 | try:
71 | signature = result.pop('key')
72 | except KeyError:
73 | raise InvalidKey() from None
74 | try:
75 | signature_bytes = b64decode(signature.encode('ascii'))
76 | except ValueError:
77 | raise InvalidKey() from None
78 | try:
79 | rsa.verify(_dumpb(result), signature_bytes, PublicKey(**pubkey_args))
80 | except VerificationError:
81 | raise InvalidKey() from None
82 | return result
83 |
84 | def _dumpb(dict_):
85 | return json.dumps(dict_, sort_keys=True).encode('utf-8')
--------------------------------------------------------------------------------
/ppg/cmdline.py:
--------------------------------------------------------------------------------
1 | from argparse import ArgumentParser
2 | from ppg._state import COMMANDS
3 | from ppg_runtime import FbsError
4 | from inspect import getfullargspec
5 | from os import getcwd
6 | from os.path import basename, splitext
7 |
8 | import ppg
9 | import logging
10 | import sys
11 |
12 | _LOG = logging.getLogger(__name__)
13 |
14 | def main(project_dir=None):
15 | """
16 | This function is executed when you run `ppg ...` on the command line. You
17 | can call this function from your own build script to run ppg as if it were
18 | called via the above command. For an example, see:
19 | https://build-system.fman.io/manual/#custom-commands
20 | """
21 | if project_dir is None:
22 | project_dir = getcwd()
23 | try:
24 | ppg.init(project_dir)
25 | # Load built-in commands:
26 | from ppg import builtin_commands
27 | from ppg.builtin_commands import _docker
28 | from ppg.builtin_commands import _gpg
29 | from ppg.builtin_commands import _account
30 | from ppg.builtin_commands import _licensing
31 | fn, args = _parse_cmdline()
32 | fn(*args)
33 | except KeyboardInterrupt:
34 | print('')
35 | sys.exit(-1)
36 | except FbsError as e:
37 | # Don't print a stack trace for FbsErrors, just their message:
38 | _LOG.error(str(e))
39 | sys.exit(-1)
40 |
41 | def command(f):
42 | """
43 | Use this as a decorator to define custom ppg commands. For an example, see:
44 | https://build-system.fman.io/manual/#custom-commands
45 | """
46 | COMMANDS[f.__name__] = f
47 | return f
48 |
49 | def _parse_cmdline():
50 | parser = _get_cmdline_parser()
51 | args = parser.parse_args()
52 | if hasattr(args, 'fn'):
53 | fn_args = []
54 | for arg in args.args[:1-len(args.defaults)]:
55 | fn_args.append(getattr(args, arg))
56 | for arg, default in zip(args.args[-len(args.defaults):], args.defaults):
57 | fn_args.append(getattr(args, arg, default))
58 | return args.fn, fn_args
59 | return parser.print_help, ()
60 |
61 | def _get_cmdline_parser():
62 | # Were we invoked with `python -m ppg`?
63 | is_python_m_fbs = splitext(basename(sys.argv[0]))[0] == '__main__'
64 | if is_python_m_fbs:
65 | prog = '%s -m ppg' % basename(sys.executable)
66 | else:
67 | prog = None
68 | parser = ArgumentParser(prog=prog, description='ppg')
69 | subparsers = parser.add_subparsers()
70 | for cmd_name, cmd_fn in COMMANDS.items():
71 | cmd_parser = subparsers.add_parser(cmd_name, help=cmd_fn.__doc__)
72 | argspec = getfullargspec(cmd_fn)
73 | args = argspec.args or []
74 | defaults = argspec.defaults or ()
75 | args_without_defaults = args[:1-len(defaults)]
76 | args_with_defaults = args[-len(defaults):]
77 | for arg in args_without_defaults:
78 | cmd_parser.add_argument(arg)
79 | for arg, default in zip(args_with_defaults, defaults):
80 | if isinstance(default, bool):
81 | cmd_parser.add_argument(
82 | '--' + arg, action='store_' + str(not default).lower()
83 | )
84 | else:
85 | type_ = None if default is None else type(default)
86 | cmd_parser.add_argument(arg, default=default, type=type_)
87 | cmd_parser.set_defaults(fn=cmd_fn, args=args, defaults=defaults)
88 | return parser
--------------------------------------------------------------------------------
/ppg/installer/linux.py:
--------------------------------------------------------------------------------
1 | from ppg import path, SETTINGS
2 | from ppg.installer import _generate_installer_resources
3 | from ppg.resources import get_icons
4 | from ppg_runtime.platform import is_arch_linux
5 | from os import makedirs, remove, rename
6 | from os.path import join, dirname, exists
7 | from shutil import copy, rmtree, copytree
8 | from subprocess import run, DEVNULL
9 |
10 | def generate_installer_files():
11 | if exists(path('target/installer')):
12 | rmtree(path('target/installer'))
13 | copytree(path('${freeze_dir}'), path('target/installer/opt/${app_name}'))
14 | _generate_installer_resources()
15 | # Special handling of the .desktop file: Replace AppName by actual name.
16 | apps_dir = path('target/installer/usr/share/applications')
17 | rename(
18 | join(apps_dir, 'AppName.desktop'),
19 | join(apps_dir, SETTINGS['app_name'] + '.desktop')
20 | )
21 | _generate_icons()
22 |
23 | def run_fpm(output_type):
24 | dest = path('target/${installer}')
25 | if exists(dest):
26 | remove(dest)
27 | # Lower-case the name to avoid the following fpm warning:
28 | # > Debian tools (dpkg/apt) don't do well with packages that use capital
29 | # > letters in the name. In some cases it will automatically downcase
30 | # > them, in others it will not. It is confusing. Best to not use any
31 | # > capital letters at all.
32 | name = SETTINGS['app_name'].lower()
33 | args = [
34 | 'fpm', '-s', 'dir',
35 | # We set the log level to error because fpm prints the following warning
36 | # even if we don't have anything in /etc:
37 | # > Debian packaging tools generally labels all files in /etc as config
38 | # > files, as mandated by policy, so fpm defaults to this behavior for
39 | # > deb packages. You can disable this default behavior with
40 | # > --deb-no-default-config-files flag
41 | '--log', 'error',
42 | '-C', path('target/installer'),
43 | '-n', name,
44 | '-v', SETTINGS['version'],
45 | '--vendor', SETTINGS['author'],
46 | '-t', output_type,
47 | '-p', dest
48 | ]
49 | if SETTINGS['description']:
50 | args.extend(['--description', SETTINGS['description']])
51 | if SETTINGS['author_email']:
52 | args.extend([
53 | '-m', '%s <%s>' % (SETTINGS['author'], SETTINGS['author_email'])
54 | ])
55 | if SETTINGS['url']:
56 | args.extend(['--url', SETTINGS['url']])
57 | for dependency in SETTINGS['depends']:
58 | args.extend(['-d', dependency])
59 | if is_arch_linux():
60 | for opt_dependency in SETTINGS['depends_opt']:
61 | args.extend(['--pacman-optional-depends', opt_dependency])
62 | try:
63 | run(args, check=True, stdout=DEVNULL)
64 | except FileNotFoundError:
65 | raise FileNotFoundError(
66 | "ppg could not find executable 'fpm'. Please install fpm using the "
67 | "instructions at "
68 | "https://fpm.readthedocs.io/en/latest/installing.html."
69 | ) from None
70 |
71 | def _generate_icons():
72 | dest_root = path('target/installer/usr/share/icons/hicolor')
73 | makedirs(dest_root)
74 | icons_fname = '%s.png' % SETTINGS['app_name']
75 | for size, _, icon_path in get_icons():
76 | icon_dest = join(dest_root, '%dx%d' % (size, size), 'apps', icons_fname)
77 | makedirs(dirname(icon_dest))
78 | copy(icon_path, icon_dest)
--------------------------------------------------------------------------------
/ppg_runtime/ReactiveWidgets/slider.py:
--------------------------------------------------------------------------------
1 | try:
2 | from PySide6.QtWidgets import QSlider
3 | from PySide6.QtCore import Qt
4 | except ImportError:
5 | try:
6 | from PySide2.QtWidgets import QSlider
7 | from PySide2.QtCore import Qt
8 | except ImportError:
9 | try:
10 | from PyQt6.QtWidgets import QSlider
11 | from PyQt6.QtCore import Qt
12 | except ImportError:
13 | try:
14 | from PyQt5.QtWidgets import QSlider
15 | from PyQt5.QtCore import Qt
16 | except ImportError:
17 | raise ImportError("No Qt bindings found.")
18 |
19 |
20 | class ReactiveSlider(QSlider):
21 | """
22 | Reactive QSlider connected to Pydux, including support for nested models.
23 |
24 | Args:
25 | parent: Parent widget that contains the store.
26 | key: Key in the store (can be nested, e.g., "settings.volume").
27 | orientation: Slider orientation (Qt.Horizontal by default).
28 | onChange: Optional callback when value changes.
29 | minimum: Minimum slider value (default 0).
30 | maximum: Maximum slider value (default 100).
31 | """
32 | def __init__(self, parent, key, orientation=None, onChange=None, minimum=0, maximum=100, **kwargs):
33 | orientation = orientation or Qt.Horizontal
34 | super().__init__(orientation, parent, **kwargs)
35 | self._store_key = key
36 | self._parent = parent
37 | self._updating_from_store = False
38 | self._onChange = onChange
39 |
40 | self.setRange(minimum, maximum)
41 |
42 | # Initialize value from store (supports nested)
43 | if hasattr(parent, "get_nested"):
44 | value = parent.get_nested(key) or minimum
45 | else:
46 | value = parent.store.get(key, minimum)
47 | self.setValue(int(value))
48 |
49 | # Connect signals
50 | self.valueChanged.connect(self._on_value_changed)
51 |
52 | # Subscribe to store changes
53 | if hasattr(parent, "subscribe_to_store"):
54 | parent.subscribe_to_store(self)
55 |
56 | def _on_value_changed(self, value):
57 | """
58 | Internal callback when the slider value changes.
59 | Updates the store (supports nested).
60 | """
61 | if self._updating_from_store:
62 | return
63 |
64 | if "." in self._store_key and hasattr(self._parent, "update_nested_model"):
65 | model_key, nested_field = self._store_key.split(".", 1)
66 | self._parent.update_nested_model(model_key, {nested_field: value})
67 | else:
68 | self._parent.update_store({self._store_key: value})
69 |
70 | if callable(self._onChange):
71 | self._onChange(value)
72 |
73 | def _update_from_store(self):
74 | """
75 | Syncs the slider value with the store.
76 | Supports nested keys using get_nested().
77 | """
78 | if hasattr(self._parent, "get_nested"):
79 | value = self._parent.get_nested(self._store_key) or self.minimum()
80 | else:
81 | value = self._parent.store.get(self._store_key, self.minimum())
82 |
83 | value = int(value)
84 | if self.value() != value:
85 | self._updating_from_store = True
86 | self.setValue(value)
87 | self._updating_from_store = False
88 |
89 | def on_store_change(self, store):
90 | """
91 | Method automatically called when the store changes.
92 | Simply refreshes the slider value from the store.
93 | """
94 | self._update_from_store()
95 |
--------------------------------------------------------------------------------
/ppg/freeze/__init__.py:
--------------------------------------------------------------------------------
1 | from ppg import path, SETTINGS
2 | from ppg._state import LOADED_PROFILES
3 | from ppg.resources import _copy
4 | from ppg_runtime._fbs import filter_public_settings
5 | from ppg_runtime._source import default_path
6 | from ppg_runtime.platform import is_mac
7 | from os import rename, makedirs
8 | from os.path import join, dirname
9 | from pathlib import PurePath
10 | from subprocess import run
11 |
12 | import ppg_runtime._frozen
13 |
14 | def run_pyinstaller(extra_args=None, debug=False):
15 | if extra_args is None:
16 | extra_args = []
17 | app_name = SETTINGS['app_name']
18 | # Would like log level WARN when not debugging. This works fine for
19 | # PyInstaller 3.3. However, for 3.4, it gives confusing warnings
20 | # "hidden import not found". So use ERROR instead.
21 | log_level = 'DEBUG' if debug else 'ERROR'
22 | args = [
23 | 'pyinstaller',
24 | '--name', app_name,
25 | '--noupx',
26 | '--log-level', log_level,
27 | '--noconfirm'
28 | ]
29 | for hidden_import in SETTINGS['hidden_imports']:
30 | args.extend(['--hidden-import', hidden_import])
31 | args.extend(SETTINGS.get('extra_pyinstaller_args', []))
32 | args.extend(extra_args)
33 | args.extend([
34 | '--distpath', path('target'),
35 | '--specpath', path('target/PyInstaller'),
36 | '--workpath', path('target/PyInstaller')
37 | ])
38 | args.extend(['--additional-hooks-dir', join(dirname(__file__), 'hooks')])
39 | if debug:
40 | args.extend(['--debug', 'all'])
41 | if is_mac():
42 | # Force generation of an .app bundle. Otherwise, PyInstaller skips
43 | # it when --debug is given.
44 | args.append('-w')
45 | hook_path = _generate_runtime_hook()
46 | args.extend(['--runtime-hook', hook_path])
47 | args.append(path(SETTINGS['main_module']))
48 | run(args, check=True)
49 | output_dir = path('target/' + app_name + ('.app' if is_mac() else ''))
50 | freeze_dir = path('${freeze_dir}')
51 | # In most cases, rename(src, dst) silently "works" when src == dst. But on
52 | # some Windows drives, it raises a FileExistsError. So check src != dst:
53 | if PurePath(output_dir) != PurePath(freeze_dir):
54 | rename(output_dir, freeze_dir)
55 |
56 | def _generate_runtime_hook():
57 | makedirs(path('target/PyInstaller'), exist_ok=True)
58 | module = ppg_runtime._frozen
59 | hook_path = path('target/PyInstaller/fbs_pyinstaller_hook.py')
60 | with open(hook_path, 'w') as f:
61 | # Inject public settings such as "version" into the binary, so
62 | # they're available at run time:
63 | f.write('\n'.join([
64 | 'import importlib',
65 | 'module = importlib.import_module(%r)' % module.__name__,
66 | 'module.BUILD_SETTINGS = %r' % filter_public_settings(SETTINGS)
67 | ]))
68 | return hook_path
69 |
70 | def _generate_resources():
71 | """
72 | Copy the data files from src/main/resources to ${freeze_dir}.
73 | Automatically filters files mentioned in the setting files_to_filter:
74 | Placeholders such as ${app_name} are automatically replaced by the
75 | corresponding setting in files on that list.
76 | """
77 | freeze_dir = path('${freeze_dir}')
78 | if is_mac():
79 | resources_dest_dir = join(freeze_dir, 'Contents', 'Resources')
80 | else:
81 | resources_dest_dir = freeze_dir
82 | for path_fn in default_path, path:
83 | for profile in LOADED_PROFILES:
84 | _copy(path_fn, 'src/main/resources/' + profile, resources_dest_dir)
85 | _copy(path_fn, 'src/freeze/' + profile, freeze_dir)
--------------------------------------------------------------------------------
/ppg_runtime/excepthook/sentry.py:
--------------------------------------------------------------------------------
1 | from ppg_runtime.excepthook import ExceptionHandler
2 | from ppg_runtime.excepthook._util import RateLimiter
3 |
4 | import sentry_sdk
5 |
6 | class SentryExceptionHandler(ExceptionHandler):
7 | """
8 | Send stack traces to Sentry. For instructions on how to set this up, see:
9 |
10 | https://build-system.fman.io/manual/#error-tracking
11 |
12 | A property of interest in this class is .scope: It lets you send additional
13 | context information to Sentry, such as the user's operating system, or their
14 | email. These data are then displayed alongside any stack traces in Sentry.
15 |
16 | A limitation of .scope is that it is only available once .init() was called.
17 | fbs's ApplicationContext performs this call automatically for handlers
18 | listed in the .exception_handlers property.
19 |
20 | The recommended way for setting context information is to use the `callback`
21 | parameter. You can see its use near the bottom of the following snippet:
22 |
23 | from fbs_runtime import platform
24 | from fbs_runtime.application_context import ApplicationContext, \
25 | cached_property, is_frozen
26 |
27 | class AppContext(ApplicationContext):
28 | ...
29 | @cached_property
30 | def exception_handlers(self):
31 | result = super().exception_handlers
32 | if is_frozen():
33 | result.append(self.sentry)
34 | return result
35 | @cached_property
36 | def sentry(self):
37 | # The Sentry client key. Eg. https://4e78a0...@sentry.io/12345.
38 | dsn = self.build_settings['sentry_dsn']
39 | # Your app version. Eg. 1.2.3:
40 | version = self.build_settings['version']
41 | # The environment in which your app is running. "local" by
42 | # default, but set to "production" when you do `fbs release`.
43 | environment = self.build_settings['environment']
44 | return SentryExceptionHandler(
45 | dsn, version, environment, callback=self._on_sentry_init
46 | )
47 | def _on_sentry_init(self):
48 | self.sentry.scope.set_extra('os', platform.name())
49 | self.sentry.scope.user = {'id': 41, 'email': 'john@gmail.com'}
50 |
51 | The optional `rate_limit` parameter to the constructor lets you limit the
52 | number of requests per minute. It is there to prevent a single client from
53 | clogging up your Sentry logs.
54 | """
55 | def __init__(
56 | self, dsn, app_version, environment, callback=lambda: None,
57 | rate_limit=10
58 | ):
59 | super().__init__()
60 | self.scope = None
61 | self._dsn = dsn
62 | self._app_version = app_version
63 | self._environment = environment
64 | self._callback = callback
65 | self._rate_limiter = RateLimiter(60, rate_limit)
66 | def init(self):
67 | sentry_sdk.init(
68 | self._dsn, release=self._app_version, environment=self._environment,
69 | attach_stacktrace=True, default_integrations=False
70 | )
71 | # Sentry doesn't give us an easy way to set context information
72 | # globally, for all threads. We work around this by maintaining a
73 | # reference to "the" main scope:
74 | self.scope = sentry_sdk.configure_scope().__enter__()
75 | self._callback()
76 | def handle(self, exc_type, exc_value, enriched_tb):
77 | if self._rate_limiter.please():
78 | sentry_sdk.capture_exception((exc_type, exc_value, enriched_tb))
--------------------------------------------------------------------------------
/ppg/_defaults/src/installer/windows/Installer.nsi:
--------------------------------------------------------------------------------
1 | !include MUI2.nsh
2 | !include FileFunc.nsh
3 | !define MUI_ICON "..\${app_name}\Icon.ico"
4 | !define MUI_UNICON "..\${app_name}\Icon.ico"
5 |
6 | !getdllversion "..\${app_name}\${app_name}.exe" ver
7 | !define VERSION "${ver1}.${ver2}.${ver3}.${ver4}"
8 |
9 | VIProductVersion "${VERSION}"
10 | VIAddVersionKey "ProductName" "${app_name}"
11 | VIAddVersionKey "FileVersion" "${VERSION}"
12 | VIAddVersionKey "ProductVersion" "${VERSION}"
13 | VIAddVersionKey "LegalCopyright" "(C) ${author}"
14 | VIAddVersionKey "FileDescription" "${app_name}"
15 |
16 | ;--------------------------------
17 | ;Perform Machine-level install, if possible
18 |
19 | !define MULTIUSER_EXECUTIONLEVEL Highest
20 | ;Add support for command-line args that let uninstaller know whether to
21 | ;uninstall machine- or user installation:
22 | !define MULTIUSER_INSTALLMODE_COMMANDLINE
23 | !include MultiUser.nsh
24 | !include LogicLib.nsh
25 |
26 | Function .onInit
27 | !insertmacro MULTIUSER_INIT
28 | ;Do not use InstallDir at all so we can detect empty $InstDir!
29 | ${If} $InstDir == "" ; /D not used
30 | ${If} $MultiUser.InstallMode == "AllUsers"
31 | StrCpy $InstDir "$PROGRAMFILES\${app_name}"
32 | ${Else}
33 | StrCpy $InstDir "$LOCALAPPDATA\${app_name}"
34 | ${EndIf}
35 | ${EndIf}
36 | FunctionEnd
37 |
38 | Function un.onInit
39 | !insertmacro MULTIUSER_UNINIT
40 | FunctionEnd
41 |
42 | ;--------------------------------
43 | ;General
44 |
45 | Name "${app_name}"
46 | OutFile "..\${installer}"
47 |
48 | ;--------------------------------
49 | ;Interface Settings
50 |
51 | !define MUI_ABORTWARNING
52 |
53 | ;--------------------------------
54 | ;Pages
55 |
56 | !define MUI_WELCOMEPAGE_TEXT "This wizard will guide you through the installation of ${app_name}.$\r$\n$\r$\n$\r$\nClick Next to continue."
57 | !insertmacro MUI_PAGE_WELCOME
58 | !insertmacro MUI_PAGE_DIRECTORY
59 | !insertmacro MUI_PAGE_INSTFILES
60 | !define MUI_FINISHPAGE_NOAUTOCLOSE
61 | !define MUI_FINISHPAGE_RUN
62 | !define MUI_FINISHPAGE_RUN_CHECKED
63 | !define MUI_FINISHPAGE_RUN_TEXT "Run ${app_name}"
64 | !define MUI_FINISHPAGE_RUN_FUNCTION "LaunchAsNonAdmin"
65 | !insertmacro MUI_PAGE_FINISH
66 |
67 | !insertmacro MUI_UNPAGE_CONFIRM
68 | !insertmacro MUI_UNPAGE_INSTFILES
69 |
70 | ;--------------------------------
71 | ;Languages
72 |
73 | !insertmacro MUI_LANGUAGE "English"
74 |
75 | ;--------------------------------
76 | ;Installer Sections
77 |
78 | !define UNINST_KEY \
79 | "Software\Microsoft\Windows\CurrentVersion\Uninstall\${app_name}"
80 | Section
81 | SetOutPath "$InstDir"
82 | File /r "..\${app_name}\*"
83 | WriteRegStr SHCTX "Software\${app_name}" "" $InstDir
84 | WriteUninstaller "$InstDir\uninstall.exe"
85 | CreateShortCut "$SMPROGRAMS\${app_name}.lnk" "$InstDir\${app_name}.exe"
86 | WriteRegStr SHCTX "${UNINST_KEY}" "DisplayName" "${app_name}"
87 | WriteRegStr SHCTX "${UNINST_KEY}" "UninstallString" \
88 | "$\"$InstDir\uninstall.exe$\" /$MultiUser.InstallMode"
89 | WriteRegStr SHCTX "${UNINST_KEY}" "QuietUninstallString" \
90 | "$\"$InstDir\uninstall.exe$\" /$MultiUser.InstallMode /S"
91 | WriteRegStr SHCTX "${UNINST_KEY}" "Publisher" "${author}"
92 | WriteRegStr SHCTX "${UNINST_KEY}" "DisplayIcon" "$InstDir\uninstall.exe"
93 | ${GetSize} "$InstDir" "/S=0K" $0 $1 $2
94 | IntFmt $0 "0x%08X" $0
95 | WriteRegDWORD SHCTX "${UNINST_KEY}" "EstimatedSize" "$0"
96 |
97 | SectionEnd
98 |
99 | ;--------------------------------
100 | ;Uninstaller Section
101 |
102 | Section "Uninstall"
103 |
104 | RMDir /r "$InstDir"
105 | Delete "$SMPROGRAMS\${app_name}.lnk"
106 | DeleteRegKey /ifempty SHCTX "Software\${app_name}"
107 | DeleteRegKey SHCTX "${UNINST_KEY}"
108 |
109 | SectionEnd
110 |
111 | Function LaunchAsNonAdmin
112 | Exec '"$WINDIR\explorer.exe" "$InstDir\${app_name}.exe"'
113 | FunctionEnd
--------------------------------------------------------------------------------
/ppg/builtin_commands/_docker.py:
--------------------------------------------------------------------------------
1 | from ppg import path, SETTINGS
2 | from ppg.builtin_commands import require_existing_project
3 | from ppg.cmdline import command
4 | from ppg.resources import _copy
5 | from ppg_runtime import FbsError
6 | from ppg_runtime._source import default_path
7 | from os import listdir
8 | from os.path import exists
9 | from shutil import rmtree
10 | from subprocess import run, CalledProcessError, PIPE
11 |
12 | import logging
13 |
14 | __all__ = ['buildvm', 'runvm']
15 |
16 | _LOG = logging.getLogger(__name__)
17 |
18 | @command
19 | def buildvm(name):
20 | """
21 | Build a Linux VM. Eg.: buildvm ubuntu
22 | """
23 | require_existing_project()
24 | build_dir = path('target/%s-docker-image' % name)
25 | if exists(build_dir):
26 | rmtree(build_dir)
27 | src_root = 'src/build/docker'
28 | available_vms = set(listdir(default_path(src_root)))
29 | if exists(path(src_root)):
30 | available_vms.update(listdir(path(src_root)))
31 | if name not in available_vms:
32 | raise FbsError(
33 | 'Could not find %s. Available VMs are:%s' %
34 | (name, ''.join(['\n * ' + vm for vm in available_vms]))
35 | )
36 | src_dir = src_root + '/' + name
37 | for path_fn in default_path, path:
38 | _copy(path_fn, src_dir, build_dir)
39 | settings = SETTINGS['docker_images'].get(name, {})
40 | for path_fn in default_path, path:
41 | for p in settings.get('build_files', []):
42 | _copy(path_fn, p, build_dir)
43 | args = ['build', '--pull', '-t', _get_docker_id(name), build_dir]
44 | for arg, value in settings.get('build_args', {}).items():
45 | args.extend(['--build-arg', '%s=%s' % (arg, value)])
46 | try:
47 | _run_docker(
48 | args, check=True, stdout=PIPE, stderr=PIPE,
49 | universal_newlines=True
50 | )
51 | except CalledProcessError as e:
52 | if '/private-key.gpg: no such file or directory' in e.stderr:
53 | message = 'Could not find private-key.gpg. Maybe you want to ' \
54 | 'run:\n ppg gengpgkey'
55 | else:
56 | message = e.stdout + '\n' + e.stderr
57 | raise FbsError(message)
58 | _LOG.info('Done. You can now execute:\n ppg runvm ' + name)
59 |
60 | @command
61 | def runvm(name):
62 | """
63 | Run a Linux VM. Eg.: runvm ubuntu
64 | """
65 | args = ['run', '-it']
66 | for item in _get_docker_mounts(name).items():
67 | args.extend(['-v', '%s:%s' % item])
68 | docker_id = _get_docker_id(name)
69 | args.append(docker_id)
70 | try:
71 | _run_docker(args, stderr=PIPE, universal_newlines=True, check=True)
72 | except CalledProcessError as e:
73 | if 'Unable to find image' in e.stderr:
74 | raise FbsError(
75 | 'Docker could not find image %s. You may want to run:\n'
76 | ' ppg buildvm %s' % (docker_id, name)
77 | )
78 |
79 | def _run_docker(args, **kwargs):
80 | try:
81 | return run(['docker'] + args, **kwargs)
82 | except FileNotFoundError:
83 | raise FbsError(
84 | 'ppg could not find Docker. Is it installed and on your PATH?'
85 | )
86 |
87 | def _get_docker_id(name):
88 | prefix = SETTINGS['app_name'].replace(' ', '_').lower()
89 | suffix = name.lower()
90 | return prefix + '/' + suffix
91 |
92 | def _get_docker_mounts(name):
93 | result = {'target/' + name.lower(): 'target'}
94 | # These directories are created inside the container by `buildvm`:
95 | ignore = {'target', 'venv'}
96 | for file_name in listdir(path('.')):
97 | if file_name in ignore:
98 | continue
99 | result[file_name] = file_name
100 | path_in_docker = lambda p: '/root/%s/%s' % (SETTINGS['app_name'], p)
101 | return {path(src): path_in_docker(dest) for src, dest in result.items()}
102 |
103 | def _get_settings(name):
104 | return SETTINGS['docker_images'][name]
--------------------------------------------------------------------------------
/ppg_runtime/ReactiveWidgets/combo_box.py:
--------------------------------------------------------------------------------
1 | try:
2 | from PySide6.QtWidgets import QComboBox
3 | except ImportError:
4 | try:
5 | from PySide2.QtWidgets import QComboBox
6 | except ImportError:
7 | try:
8 | from PyQt6.QtWidgets import QComboBox
9 | except ImportError:
10 | try:
11 | from PyQt5.QtWidgets import QComboBox
12 | except ImportError:
13 | raise ImportError("No Qt bindings found.")
14 |
15 |
16 | class ReactiveComboBox(QComboBox):
17 | """
18 | Reactive QComboBox connected to Pydux, including support for nested models.
19 |
20 | Args:
21 | parent: Parent widget that contains the store.
22 | key: Key in the store for the selection (can be nested, e.g., "user.country").
23 | items: Fixed list of options (optional).
24 | dynamic_items: Key in the store that provides a dynamic list of options (optional).
25 | onChange: Optional callback when selection changes.
26 | """
27 | def __init__(self, parent, key, items=None, dynamic_items=None, onChange=None, **kwargs):
28 | super().__init__(parent, **kwargs)
29 | self._parent = parent
30 | self._store_key = key
31 | self._items_key = dynamic_items
32 | self._onChange = onChange
33 | self._updating_from_store = False
34 |
35 | # Load initial items
36 | self._static_items = items or []
37 | self._update_items()
38 |
39 | # Set initial selection from store
40 | value = self._parent.get_nested(key) or ""
41 | if value in self._get_items():
42 | self.setCurrentText(str(value))
43 |
44 | # Connect signal for selection change
45 | self.currentTextChanged.connect(self._on_text_changed)
46 |
47 | # Subscribe to store changes if parent supports it
48 | if hasattr(parent, "subscribe_to_store"):
49 | parent.subscribe_to_store(self)
50 |
51 | def _get_items(self):
52 | """Returns current list of items (dynamic from store if items_key is defined)."""
53 | if self._items_key:
54 | dynamic_list = self._parent.get_nested(self._items_key)
55 | if isinstance(dynamic_list, list):
56 | return dynamic_list
57 | return self._static_items
58 |
59 | def _update_items(self):
60 | """Updates ComboBox items from store or fixed list."""
61 | current = self.currentText()
62 | self.blockSignals(True)
63 | self.clear()
64 | self.addItems([str(i) for i in self._get_items()])
65 | # Restore selection if still exists
66 | if current in self._get_items():
67 | self.setCurrentText(current)
68 | self.blockSignals(False)
69 |
70 | def _on_text_changed(self, text):
71 | """
72 | Internal callback when ComboBox selection changes.
73 | Updates the store (supports nested keys).
74 | """
75 | if self._updating_from_store:
76 | return
77 |
78 | if "." in self._store_key:
79 | model_key, nested_field = self._store_key.split(".", 1)
80 | if hasattr(self._parent, "update_nested_model"):
81 | self._parent.update_nested_model(model_key, {nested_field: text})
82 | else:
83 | self._parent.update_store({self._store_key: text})
84 |
85 | if callable(self._onChange):
86 | self._onChange(text)
87 |
88 | def _update_from_store(self):
89 | """Synchronizes ComboBox selection and items with the store."""
90 | # Update items if dynamic items key is defined
91 | if self._items_key:
92 | self._update_items()
93 | # Update selection
94 | value = str(self._parent.get_nested(self._store_key) or "")
95 | if self.currentText() != value:
96 | self._updating_from_store = True
97 | self.setCurrentText(value)
98 | self._updating_from_store = False
99 |
100 | def on_store_change(self, store):
101 | """Method automatically called when the store changes."""
102 | self._update_from_store()
103 |
--------------------------------------------------------------------------------
/ppg/freeze/windows.py:
--------------------------------------------------------------------------------
1 | from ppg import path, SETTINGS
2 | from ppg.freeze import run_pyinstaller, _generate_resources
3 | from ppg.resources import _copy
4 | from ppg_runtime._source import default_path
5 | from os import remove
6 | from os.path import join, exists
7 | from shutil import copy
8 |
9 | import os
10 | import struct
11 | import sys
12 |
13 | def freeze_windows(debug=False):
14 | args = []
15 | if not (debug or SETTINGS['show_console_window']):
16 | # The --windowed flag below prevents us from seeing any console output.
17 | # We therefore only add it when we're not debugging.
18 | args.append('--windowed')
19 | args.extend(['--icon', path('src/main/icons/Icon.ico')])
20 | for path_fn in default_path, path:
21 | _copy(path_fn, 'src/freeze/windows/version_info.py', path('target/PyInstaller'))
22 | args.extend(['--version-file', path('target/PyInstaller/version_info.py')])
23 | run_pyinstaller(args, debug)
24 | _restore_corrupted_python_dlls()
25 | _generate_resources()
26 | copy(path('src/main/icons/Icon.ico'), path('${freeze_dir}'))
27 | _add_missing_dlls()
28 |
29 | def _restore_corrupted_python_dlls():
30 | # PyInstaller <= 3.4 somehow corrupts python3*.dll - see:
31 | # https://github.com/pyinstaller/pyinstaller/issues/2526
32 | # Restore the uncorrupted original:
33 | python_dlls = (
34 | 'python%s.dll' % sys.version_info.major,
35 | 'python%s%s.dll' % (sys.version_info.major, sys.version_info.minor)
36 | )
37 | for dll_name in python_dlls:
38 | try:
39 | remove(path('${freeze_dir}/' + dll_name))
40 | except FileNotFoundError:
41 | pass
42 | else:
43 | copy(_find_on_path(dll_name), path('${freeze_dir}'))
44 |
45 | def _add_missing_dlls():
46 | for dll_name in (
47 | 'msvcr100.dll', 'msvcr110.dll', 'msvcp110.dll', 'vcruntime140.dll',
48 | 'msvcp140.dll', 'concrt140.dll', 'vccorlib140.dll'
49 | ):
50 | try:
51 | _add_missing_dll(dll_name)
52 | except LookupError:
53 | raise FileNotFoundError(
54 | "Could not find %s on your PATH. Please install the Visual C++ "
55 | "Redistributable for Visual Studio 2012 from:\n "
56 | "https://www.microsoft.com/en-us/download/details.aspx?id=30679"
57 | % dll_name
58 | ) from None
59 | for ucrt_dll in ('api-ms-win-crt-multibyte-l1-1-0.dll',):
60 | try:
61 | _add_missing_dll(ucrt_dll)
62 | except LookupError:
63 | bitness_32_or_64 = struct.calcsize("P") * 8
64 | raise FileNotFoundError(
65 | "Could not find %s on your PATH. If you are on Windows 10, you "
66 | "may have to install the Windows 10 SDK from "
67 | "https://developer.microsoft.com/en-us/windows/downloads/windows-10-sdk. "
68 | "Otherwise, try installing KB2999226 from "
69 | "https://support.microsoft.com/en-us/kb/2999226. "
70 | "In both cases, add the directory containing %s to your PATH "
71 | "environment variable afterwards. If there are 32 and 64 bit "
72 | "versions of the DLL, use the %s bit one (because that's the "
73 | "bitness of your current Python interpreter)." % (
74 | ucrt_dll, ucrt_dll, bitness_32_or_64
75 | )
76 | ) from None
77 |
78 | def _add_missing_dll(dll_name):
79 | freeze_dir = path('${freeze_dir}')
80 | if not exists(join(freeze_dir, dll_name)):
81 | copy(_find_on_path(dll_name), freeze_dir)
82 |
83 | def _find_on_path(file_name):
84 | path = os.environ.get("PATH", os.defpath)
85 | path_items = path.split(os.pathsep)
86 | if sys.platform == "win32":
87 | if not os.curdir in path_items:
88 | path_items.insert(0, os.curdir)
89 | seen = set()
90 | for dir_ in path_items:
91 | normdir = os.path.normcase(dir_)
92 | if not normdir in seen:
93 | seen.add(normdir)
94 | file_path = join(dir_, file_name)
95 | if exists(file_path):
96 | return file_path
97 | raise LookupError("Could not find %s on PATH" % file_name)
--------------------------------------------------------------------------------
/ppg/sign/windows.py:
--------------------------------------------------------------------------------
1 | from ppg import path, SETTINGS
2 | from ppg_runtime import FbsError
3 | from os import makedirs
4 | from os.path import join, splitext, dirname, basename, exists
5 | from shutil import copy
6 | from subprocess import call, run, DEVNULL
7 |
8 | import hashlib
9 | import json
10 | import os
11 |
12 | _CERTIFICATE_PATH = 'src/sign/windows/certificate.pfx'
13 | _TO_SIGN = ('.exe', '.cab', '.dll', '.ocx', '.msi', '.xpi')
14 |
15 | def sign_windows():
16 | if not exists(path(_CERTIFICATE_PATH)):
17 | raise FbsError(
18 | 'Could not find a code signing certificate at:\n '
19 | + _CERTIFICATE_PATH
20 | )
21 | if 'windows_sign_pass' not in SETTINGS:
22 | raise FbsError(
23 | "Please set 'windows_sign_pass' to the password of %s in either "
24 | "src/build/settings/secret.json, .../windows.json or .../base.json."
25 | % _CERTIFICATE_PATH
26 | )
27 | for subdir, _, files in os.walk(path('${freeze_dir}')):
28 | for file_ in files:
29 | extension = splitext(file_)[1]
30 | if extension in _TO_SIGN:
31 | sign_file(join(subdir, file_))
32 |
33 | def sign_file(file_path, description='', url=''):
34 | helper = _SignHelper.instance()
35 | if not helper.is_signed(file_path):
36 | helper.sign(file_path, description, url)
37 |
38 | class _SignHelper:
39 |
40 | _INSTANCE = None
41 |
42 | @classmethod
43 | def instance(cls):
44 | if cls._INSTANCE is None:
45 | cls._INSTANCE = cls(path('cache/signed'))
46 | return cls._INSTANCE
47 |
48 | def __init__(self, cache_dir):
49 | self._cache_dir = cache_dir
50 |
51 | def is_signed(self, file_path):
52 | return not call(
53 | ['signtool', 'verify', '/pa', file_path], stdout=DEVNULL,
54 | stderr=DEVNULL
55 | )
56 |
57 | def sign(self, file_path, description, url):
58 | json_path = self._get_json_path(file_path)
59 | try:
60 | with open(json_path) as f:
61 | cached = json.load(f)
62 | is_in_cache = description == cached['description'] and \
63 | url == cached['url'] and \
64 | self._hash(file_path) == cached['hash']
65 | except FileNotFoundError:
66 | is_in_cache = False
67 | if not is_in_cache:
68 | self._sign(file_path, description, url)
69 | copy(self._get_path_in_cache(file_path), file_path)
70 |
71 | def _sign(self, file_path, description, url):
72 | path_in_cache = self._get_path_in_cache(file_path)
73 | makedirs(dirname(path_in_cache), exist_ok=True)
74 | copy(file_path, path_in_cache)
75 | hash_ = self._hash(path_in_cache)
76 | self._run_signtool(path_in_cache, 'sha1')
77 | self._run_signtool(path_in_cache, 'sha256')
78 | with open(self._get_json_path(file_path), 'w') as f:
79 | json.dump({
80 | 'description': description,
81 | 'url': url,
82 | 'hash': hash_
83 | }, f)
84 |
85 | def _get_json_path(self, file_path):
86 | return self._get_path_in_cache(file_path) + '.json'
87 |
88 | def _get_path_in_cache(self, file_path):
89 | return join(self._cache_dir, basename(file_path))
90 |
91 | def _run_signtool(self, file_path, digest_alg, description='', url=''):
92 | password = SETTINGS['windows_sign_pass']
93 | args = [
94 | 'signtool', 'sign', '/f', path(_CERTIFICATE_PATH), '/p', password
95 | ]
96 | if 'windows_sign_server' in SETTINGS:
97 | args.extend(['/tr', SETTINGS['windows_sign_server']])
98 | if description:
99 | args.extend(['/d', description])
100 | if url:
101 | args.extend(['/du', url])
102 | args.extend(['/as', '/fd', digest_alg, '/td', digest_alg])
103 | args.append(file_path)
104 | run(args, check=True, stdout=DEVNULL)
105 |
106 | def _hash(self, file_path):
107 | bufsize = 65536
108 | hasher = hashlib.md5()
109 | with open(file_path, 'rb') as f:
110 | buf = f.read(bufsize)
111 | while buf:
112 | hasher.update(buf)
113 | buf = f.read(bufsize)
114 | return hasher.hexdigest()
--------------------------------------------------------------------------------
/examples/hybryd-app/src/main/python/main.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from ppg_runtime.application_context.PySide6 import ApplicationContext
3 | from ppg_runtime.application_context import PPGLifeCycle, Pydux, init_lifecycle, BridgeManager
4 | from ppg_runtime.application_context.devtools.reloader import hot_reloading
5 | from ppg_runtime.application_context.utils import app_is_frozen
6 | from PySide6.QtWidgets import QMainWindow, QPushButton
7 | from PySide6.QtCore import QUrl
8 | from PySide6.QtWebEngineWidgets import QWebEngineView
9 | from PySide6.QtWebEngineCore import QWebEnginePage
10 |
11 | # --------------------------------------------------------------------------------------
12 | # Important! Production Considerations for Hot Reloading
13 | # --------------------------------------------------------------------------------------
14 | # Hot reloading is a development tool that allows you to instantly see UI changes
15 | # when you save a file. It's extremely useful for rapid prototyping and designing
16 | # interfaces.
17 | #
18 | # However, this functionality is not designed for use in production environments.
19 | # For the final version of your application, it is highly recommended to remove
20 | # the code related to hot reloading, such as the `@hot_reloading` decorator
21 | # and the `window._init_hot_reload_system(__file__)` call.
22 | #
23 | # Keeping hot reloading active in production can negatively impact the application's
24 | # performance, stability, and security.
25 | # --------------------------------------------------------------------------------------
26 |
27 | @init_lifecycle
28 | @hot_reloading
29 | class Pwa(QMainWindow, PPGLifeCycle, Pydux):
30 | def component_will_mount(self):
31 | self.subscribe_to_store(self)
32 |
33 | def render_(self):
34 |
35 | # loads the embedded browser engine (WebEngineView) where the PWA will be displayed (index.html)
36 | self.engine = QWebEngineView(self)
37 | self.engine.load(QUrl.fromLocalFile(self.get_resource("index.html")))
38 |
39 |
40 | # Create a bridge for Python ↔ JavaScript communication using QWebChannel
41 | # You can create multiple bridges if needed by instantiating BridgeManager with different names
42 | self.bridge = BridgeManager(self.engine, "bridge")
43 |
44 | # Register an event called "callback"
45 | # When triggered from JS, it will call handle_callback() in Python
46 | self.bridge.register("callback", self.handle_callback)
47 |
48 |
49 | # Add buttons to demonstrate functionality (e.g., opening DevTools, emitting events to JS)
50 | self.button = QPushButton("Open DevTools", self, clicked=self.open_dev_tools)
51 | self.button2 = QPushButton("test emit", self, clicked=lambda: self.bridge.emit("callback", {"data": "Hello from Python!"}))
52 |
53 | self.setCentralWidget(self.engine)
54 |
55 | def handle_callback(self, payload):
56 | """This is a callback function that handles messages from JavaScript via the bridge.
57 |
58 | Args:
59 | payload (dict): The payload data sent from JavaScript.
60 |
61 | Returns:
62 | str: A response message indicating the result of the callback handling.
63 | """
64 |
65 | print("Received message from JS:", payload)
66 |
67 | # You can send back a response to JavaScript if needed by returning a value
68 | return f"USER: {payload['username']} logged in successfully! (handled by Python)"
69 |
70 | def open_dev_tools(self):
71 | """Opens the DevTools window for the embedded web engine."""
72 |
73 | self.dev_tools = QWebEngineView(self)
74 | if not self.engine.page().devToolsPage():
75 | self.engine.page().setDevToolsPage(QWebEnginePage(self.engine.page().profile(), self.dev_tools))
76 | self.dev_tools.setPage(self.engine.page().devToolsPage())
77 | self.dev_tools.setWindowTitle("DevTools")
78 | self.dev_tools.show()
79 | self.dev_tools.resize(500, self.height())
80 | self.dev_tools.move(self.width() - 500, 0)
81 |
82 |
83 | # Emit an event to JavaScript indicating that DevTools has been opened (for demonstration purposes)
84 | self.bridge.emit("devtools_opened", {"message": "DevTools opened"})
85 |
86 | def responsive_UI(self):
87 | self.setMinimumSize(640, 480)
88 | self.button2.move(100, 0)
89 |
90 |
91 | if __name__ == '__main__':
92 | appctxt = ApplicationContext()
93 | window = Pwa()
94 | if not app_is_frozen():
95 | window._init_hot_reload_system(__file__)
96 | window.show()
97 | exec_func = getattr(appctxt.app, 'exec', appctxt.app.exec_)
98 | sys.exit(exec_func())
--------------------------------------------------------------------------------
/ppg/resources.py:
--------------------------------------------------------------------------------
1 | from ppg import path, SETTINGS
2 | from ppg_runtime import FbsError
3 | from ppg._state import LOADED_PROFILES
4 | from glob import glob
5 | from os import makedirs
6 | from os.path import dirname, isfile, join, basename, relpath, splitext, exists
7 | from pathlib import Path
8 | from shutil import copy, copymode
9 |
10 | import re
11 | import os
12 |
13 | def copy_with_filtering(
14 | src_dir_or_file, dest_dir, replacements=None, files_to_filter=None,
15 | exclude=None, placeholder='${%s}'
16 | ):
17 | """
18 | Copy the given file or directory to the given destination, optionally
19 | applying filtering.
20 | """
21 | if replacements is None:
22 | replacements = SETTINGS
23 | if files_to_filter is None:
24 | files_to_filter = []
25 | if exclude is None:
26 | exclude = []
27 | to_copy = _get_files_to_copy(src_dir_or_file, dest_dir, exclude)
28 | to_filter = _paths(files_to_filter)
29 | for src, dest in to_copy:
30 | makedirs(dirname(dest), exist_ok=True)
31 | if files_to_filter is None or src in to_filter:
32 | _copy_with_filtering(src, dest, replacements, placeholder)
33 | else:
34 | copy(src, dest)
35 |
36 | def get_icons():
37 | """
38 | Return a list [(size, scale, path)] of available app icons for the current
39 | platform.
40 | """
41 | result = {}
42 | for profile in LOADED_PROFILES:
43 | icons_dir = 'src/main/icons/' + profile
44 | for icon_path in glob(path(icons_dir + '/*.png')):
45 | name = splitext(basename(icon_path))[0]
46 | match = re.match('(\d+)(?:@(\d+)x)?', name)
47 | if not match:
48 | raise FbsError('Invalid icon name: ' + icon_path)
49 | size, scale = int(match.group(1)), int(match.group(2) or '1')
50 | result[(size, scale)] = icon_path
51 | return [(size, scale, path) for (size, scale), path in result.items()]
52 |
53 | def _get_files_to_copy(src_dir_or_file, dest_dir, exclude):
54 | excludes = _paths(map(path, exclude))
55 | if isfile(src_dir_or_file) and src_dir_or_file not in excludes:
56 | yield src_dir_or_file, join(dest_dir, basename(src_dir_or_file))
57 | else:
58 | for (subdir, _, files) in os.walk(src_dir_or_file):
59 | dest_subdir = join(dest_dir, relpath(subdir, src_dir_or_file))
60 | for file_ in files:
61 | file_path = join(subdir, file_)
62 | dest_path = join(dest_subdir, file_)
63 | if file_path not in excludes:
64 | yield file_path, dest_path
65 |
66 | def _copy_with_filtering(
67 | src_file, dest_file, dict_, placeholder='${%s}', encoding='utf-8'
68 | ):
69 | replacements = []
70 | for key, value in dict_.items():
71 | old = (placeholder % key).encode(encoding)
72 | new = str(value).encode(encoding)
73 | replacements.append((old, new))
74 | with open(src_file, 'rb') as open_src_file:
75 | with open(dest_file, 'wb') as open_dest_file:
76 | for line in open_src_file:
77 | new_line = line
78 | for old, new in replacements:
79 | new_line = new_line.replace(old, new)
80 | open_dest_file.write(new_line)
81 | copymode(src_file, dest_file)
82 |
83 | class _paths:
84 | def __init__(self, paths):
85 | self._paths = []
86 | # _defaults includes "files_to_filter" - eg. Installer.nsi. If these
87 | # files don't also exist in the "user's" src/ directory, then
88 | # Path(p).resolve() raises FileNotFoundError. Handle this:
89 | for p in paths:
90 | try:
91 | self._paths.append(self._resolve_strict(Path(p)))
92 | except FileNotFoundError:
93 | pass
94 | def __contains__(self, item):
95 | item = Path(item).resolve()
96 | for p in self._paths:
97 | # We do `p == item` here instead of `p.samefile(item)` because a
98 | # user reported that the former does not work. The affected paths
99 | # were in a VirtualBox shared folder in a Windows guest. They had
100 | # different st_ino values even though the paths were the same.
101 | # See https://github.com/mherrmann/fbs/issues/112.
102 | if p == item or p in item.parents:
103 | return True
104 | return False
105 | def _resolve_strict(self, path_):
106 | try:
107 | return path_.resolve(strict=True)
108 | except TypeError:
109 | # Python < 3.6:
110 | return path_.resolve()
111 |
112 | def _copy(path_fn, src, dst): # Used by several other internal ppg modules
113 | src = path_fn(src)
114 | if exists(src):
115 | filter_ = [path_fn(f) for f in SETTINGS['files_to_filter']]
116 | copy_with_filtering(src, dst, files_to_filter=filter_)
117 | return True
118 | return False
--------------------------------------------------------------------------------