├── 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 |
Iconos diseñados por samlakodad from www.flaticon.es
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 |
Iconos diseñados por samlakodad from www.flaticon.es
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 |
Iconos diseñados por samlakodad from www.flaticon.es
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 |
Iconos diseñados por samlakodad from www.flaticon.es
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 |
Iconos diseñados por samlakodad from www.flaticon.es
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 |
Iconos diseñados por samlakodad from www.flaticon.es
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 |
17 | 18 |

19 | 20 |

21 | 22 | 23 | 24 |
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 --------------------------------------------------------------------------------