example placeholder logo #1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build Binaries | ||
| on: | ||
| push: | ||
| branches: | ||
| - main | ||
| workflow_dispatch: | ||
| jobs: | ||
| build: | ||
| strategy: | ||
| matrix: | ||
| os: [ubuntu-latest, windows-latest, macos-latest] | ||
| runs-on: ${{ matrix.os }} | ||
| steps: | ||
| - name: Checkout code | ||
| uses: actions/checkout@v3 | ||
| - name: Set up Python | ||
| uses: actions/setup-python@v4 | ||
| with: | ||
| python-version: '3.10' | ||
| - name: Load build configuration | ||
| run: | | ||
| python3 -c " | ||
| import os | ||
| import sys | ||
| # Read configuration file | ||
| config = {} | ||
| try: | ||
| with open('build-config.txt', 'r', encoding='utf-8') as f: | ||
| for line in f: | ||
| line = line.strip() | ||
| if line and not line.startswith('#') and '=' in line: | ||
| key, value = line.split('=', 1) | ||
| config[key.strip()] = value.strip().strip('\"\'') | ||
| except FileNotFoundError: | ||
| print('build-config.txt not found!') | ||
| sys.exit(1) | ||
| # Set environment variables | ||
| required_keys = ['APP_NAME', 'APP_VERSION', 'APP_AUTHOR', 'APP_DESCRIPTION', 'APP_URL'] | ||
| for key in required_keys: | ||
| if key not in config: | ||
| print(f'Missing required configuration: {key}') | ||
| sys.exit(1) | ||
| # Write to GitHub environment | ||
| with open(os.environ['GITHUB_ENV'], 'a') as env_file: | ||
| env_file.write(f'{key}={config[key]}\n') | ||
| # Optional configurations with defaults | ||
| optional_configs = { | ||
| 'MAIN_SCRIPT': 'src/main.py', | ||
| 'ICON_PATH': 'assets/logo.png', | ||
| 'LICENSE_FILE': 'LICENSE', | ||
| 'DMG_BACKGROUND': 'assets/dmg-background.png', | ||
| 'BUILD_ONEFILE': 'true' | ||
| } | ||
| for key, default in optional_configs.items(): | ||
| value = config.get(key, default) | ||
| with open(os.environ['GITHUB_ENV'], 'a') as env_file: | ||
| env_file.write(f'{key}={value}\n') | ||
| print('Configuration loaded successfully') | ||
| print(f'Building: {config[\"APP_NAME\"]} v{config[\"APP_VERSION\"]}') | ||
| " | ||
| shell: bash | ||
| - name: Install dependencies | ||
| run: | | ||
| python -m pip install --upgrade pip | ||
| pip install -r requirements.txt | ||
| pip install pyinstaller pillow | ||
| # Windows-specific: Install NSIS for installer creation | ||
| - name: Install NSIS (Windows only) | ||
| if: runner.os == 'Windows' | ||
| run: | | ||
| choco install nsis -y | ||
| # Linux-specific: Install AppImage tools | ||
| - name: Install AppImage tools (Linux only) | ||
| if: runner.os == 'Linux' | ||
| run: | | ||
| sudo apt-get update | ||
| sudo apt-get install -y fuse libfuse2 file | ||
| # Download AppImageTool | ||
| wget -O appimagetool https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage | ||
| chmod +x appimagetool | ||
| if ! ./appimagetool --help >/dev/null 2>&1; then | ||
| echo "Direct execution failed, extracting AppImageTool..." | ||
| ./appimagetool --appimage-extract | ||
| chmod +x squashfs-root/AppRun | ||
| sudo cp squashfs-root/AppRun /usr/local/bin/appimagetool | ||
| sudo chmod +x /usr/local/bin/appimagetool | ||
| else | ||
| echo "Direct execution works, moving to /usr/local/bin" | ||
| sudo mv appimagetool /usr/local/bin/appimagetool | ||
| fi | ||
| - name: Build with PyInstaller | ||
| run: | | ||
| # Determine PyInstaller flags | ||
| PYINSTALLER_FLAGS="--windowed --distpath dist" | ||
| if [[ "$BUILD_ONEFILE" == "true" && "$RUNNER_OS" != "macOS" ]]; then | ||
| PYINSTALLER_FLAGS="$PYINSTALLER_FLAGS --onefile" | ||
| fi | ||
| # Handle icon conversion and usage | ||
| ICON_FLAG="" | ||
| if [ -f "$ICON_PATH" ]; then | ||
| if [[ "$RUNNER_OS" == "macOS" ]]; then | ||
| # Convert PNG to ICNS for macOS | ||
| mkdir -p ${APP_NAME}.iconset | ||
| sips -z 16 16 "$ICON_PATH" --out ${APP_NAME}.iconset/icon_16x16.png | ||
| sips -z 32 32 "$ICON_PATH" --out ${APP_NAME}.iconset/icon_16x16@2x.png | ||
| sips -z 32 32 "$ICON_PATH" --out ${APP_NAME}.iconset/icon_32x32.png | ||
| sips -z 64 64 "$ICON_PATH" --out ${APP_NAME}.iconset/icon_32x32@2x.png | ||
| sips -z 128 128 "$ICON_PATH" --out ${APP_NAME}.iconset/icon_128x128.png | ||
| sips -z 256 256 "$ICON_PATH" --out ${APP_NAME}.iconset/icon_128x128@2x.png | ||
| sips -z 256 256 "$ICON_PATH" --out ${APP_NAME}.iconset/icon_256x256.png | ||
| sips -z 512 512 "$ICON_PATH" --out ${APP_NAME}.iconset/icon_256x256@2x.png | ||
| sips -z 512 512 "$ICON_PATH" --out ${APP_NAME}.iconset/icon_512x512.png | ||
| cp "$ICON_PATH" ${APP_NAME}.iconset/icon_512x512@2x.png | ||
| iconutil -c icns ${APP_NAME}.iconset | ||
| ICON_FLAG="--icon=${APP_NAME}.icns" | ||
| elif [[ "$RUNNER_OS" == "Windows" ]]; then | ||
| # Convert PNG to ICO for Windows | ||
| python -c " | ||
| from PIL import Image | ||
| import sys | ||
| try: | ||
| img = Image.open('$ICON_PATH') | ||
| img.save('${APP_NAME}.ico', format='ICO', sizes=[(16,16), (32,32), (48,48), (64,64), (128,128), (256,256)]) | ||
| print('Icon converted successfully') | ||
| except Exception as e: | ||
| print(f'Icon conversion failed: {e}') | ||
| sys.exit(0) | ||
| " | ||
| if [ -f "${APP_NAME}.ico" ]; then | ||
| ICON_FLAG="--icon=${APP_NAME}.ico" | ||
| fi | ||
| fi | ||
| fi | ||
| # Build the application | ||
| pyinstaller $PYINSTALLER_FLAGS $ICON_FLAG "$MAIN_SCRIPT" --name "$APP_NAME" | ||
| shell: bash | ||
| # Create AppImage for Linux | ||
| - name: Create AppImage (Linux only) | ||
| if: runner.os == 'Linux' | ||
| run: | | ||
| APPDIR=dist/${APP_NAME}.AppDir | ||
| # Create AppDir structure | ||
| mkdir -p "$APPDIR/usr/bin" | ||
| mkdir -p "$APPDIR/usr/share/applications" | ||
| mkdir -p "$APPDIR/usr/share/icons/hicolor/256x256/apps" | ||
| # Copy the binary | ||
| cp "dist/${APP_NAME}" "$APPDIR/usr/bin/" | ||
| chmod +x "$APPDIR/usr/bin/${APP_NAME}" | ||
| # Copy icon if it exists | ||
| if [ -f "$ICON_PATH" ]; then | ||
| cp "$ICON_PATH" "$APPDIR/usr/share/icons/hicolor/256x256/apps/${APP_NAME}.png" | ||
| cp "$ICON_PATH" "$APPDIR/${APP_NAME}.png" | ||
| fi | ||
| # Create .desktop file | ||
| cat > "$APPDIR/usr/share/applications/${APP_NAME}.desktop" << DESKTOP_EOF | ||
| [Desktop Entry] | ||
| Type=Application | ||
| Name=${APP_NAME} | ||
| Comment=${APP_DESCRIPTION} | ||
| Exec=${APP_NAME} | ||
| Icon=${APP_NAME} | ||
| Categories=Utility; | ||
| Terminal=false | ||
| DESKTOP_EOF | ||
| # Copy .desktop file to AppDir root | ||
| cp "$APPDIR/usr/share/applications/${APP_NAME}.desktop" "$APPDIR/" | ||
| chmod +x "$APPDIR/${APP_NAME}.desktop" | ||
| # Create AppRun script | ||
| cat > "$APPDIR/AppRun" << APPRUN_EOF | ||
| #!/bin/bash | ||
| HERE="\$(dirname "\$(readlink -f "\${0}")")" | ||
| EXEC="\${HERE}/usr/bin/${APP_NAME}" | ||
| exec "\${EXEC}" "\$@" | ||
| APPRUN_EOF | ||
| chmod +x "$APPDIR/AppRun" | ||
| # Create the AppImage | ||
| if command -v appimagetool >/dev/null 2>&1; then | ||
| appimagetool "$APPDIR" "dist/${APP_NAME}.AppImage" | ||
| elif [ -f "squashfs-root/AppRun" ]; then | ||
| squashfs-root/AppRun "$APPDIR" "dist/${APP_NAME}.AppImage" | ||
| else | ||
| echo "No appimagetool found, creating tar.gz instead" | ||
| cd dist | ||
| tar -czf "${APP_NAME}-linux.tar.gz" "${APP_NAME}.AppDir" | ||
| cd .. | ||
| fi | ||
| # Create AppleScript for DMG styling | ||
| - name: Create AppleScript for DMG (macOS only) | ||
| if: runner.os == 'macOS' | ||
| run: | | ||
| cat > dmg_setup.applescript << APPLESCRIPT_EOF | ||
| tell application "Finder" | ||
| tell disk "Install ${APP_NAME}" | ||
| open | ||
| set current view of container window to icon view | ||
| set toolbar visible of container window to false | ||
| set statusbar visible of container window to false | ||
| set the bounds of container window to {100, 100, 900, 600} | ||
| set viewOptions to the icon view options of container window | ||
| set arrangement of viewOptions to not arranged | ||
| set icon size of viewOptions to 128 | ||
| set background picture of viewOptions to file ".background:background.png" | ||
| set position of item "${APP_NAME}.app" to {200, 300} | ||
| set position of item "Applications" to {600, 300} | ||
| close | ||
| open | ||
| update without registering applications | ||
| delay 2 | ||
| end tell | ||
| end tell | ||
| APPLESCRIPT_EOF | ||
| - name: Create DMG (macOS only) | ||
| if: runner.os == 'macOS' | ||
| run: | | ||
| APP_PATH=dist/${APP_NAME}.app | ||
| DMG_NAME=${APP_NAME}-${APP_VERSION}.dmg | ||
| VOL_NAME="Install ${APP_NAME}" | ||
| DMG_TEMP=tmp.dmg | ||
| STAGING_DIR=dist/dmg-staging | ||
| # Create staging directory | ||
| mkdir -p "$STAGING_DIR/.background" | ||
| cp -R "$APP_PATH" "$STAGING_DIR/" | ||
| ln -s /Applications "$STAGING_DIR/Applications" | ||
| # Use custom background if available, otherwise create default | ||
| if [ -f "$DMG_BACKGROUND" ]; then | ||
| cp "$DMG_BACKGROUND" "$STAGING_DIR/.background/background.png" | ||
| else | ||
| python3 -c " | ||
| from PIL import Image | ||
| img = Image.new('RGB', (900, 600), color='#f0f0f0') | ||
| img.save('$STAGING_DIR/.background/background.png') | ||
| " || echo "No PIL available, skipping background" | ||
| fi | ||
| # Create and style DMG | ||
| hdiutil create -srcfolder "$STAGING_DIR" -volname "$VOL_NAME" -fs HFS+ \ | ||
| -fsargs "-c c=64,a=16,e=16" -format UDRW -ov "$DMG_TEMP" | ||
| MOUNT_DIR="/Volumes/$VOL_NAME" | ||
| if [ -d "$MOUNT_DIR" ]; then | ||
| hdiutil detach "$MOUNT_DIR" -force || true | ||
| sleep 1 | ||
| fi | ||
| hdiutil attach -readwrite -nobrowse -noverify -mountpoint "$MOUNT_DIR" "$DMG_TEMP" | ||
| if [ -d "$MOUNT_DIR" ] && [ -d "$MOUNT_DIR/${APP_NAME}.app" ]; then | ||
| osascript dmg_setup.applescript || echo "AppleScript failed, continuing..." | ||
| sleep 3 | ||
| sync | ||
| for i in {1..5}; do | ||
| if hdiutil detach "$MOUNT_DIR" 2>/dev/null; then | ||
| break | ||
| elif [ $i -eq 5 ]; then | ||
| hdiutil detach "$MOUNT_DIR" -force || true | ||
| sleep 2 | ||
| break | ||
| else | ||
| sleep 2 | ||
| fi | ||
| done | ||
| fi | ||
| sleep 2 | ||
| hdiutil convert "$DMG_TEMP" -format UDZO -imagekey zlib-level=9 -o "dist/${DMG_NAME}" || \ | ||
| hdiutil create -srcfolder "$STAGING_DIR" -volname "$VOL_NAME" -format UDZO -o "dist/${DMG_NAME}" | ||
| rm -f "$DMG_TEMP" | ||
| rm -rf "$STAGING_DIR" | ||
| rm -f dmg_setup.applescript | ||
| # Create NSIS installer script | ||
| - name: Create NSIS installer script (Windows only) | ||
| if: runner.os == 'Windows' | ||
| run: | | ||
|
Check failure on line 310 in .github/workflows/build.yml
|
||
| python -c " | ||
| import os | ||
| nsis_content = f'''!define APP_NAME \"${{ env.APP_NAME }}\" | ||
| !define APP_VERSION \"${{ env.APP_VERSION }}\" | ||
| !define APP_PUBLISHER \"${{ env.APP_AUTHOR }}\" | ||
| !define APP_URL \"${{ env.APP_URL }}\" | ||
| !define APP_EXE \"${{ env.APP_NAME }}.exe\" | ||
| ; Installer attributes | ||
| Name \"\${{APP_NAME}}\" | ||
| OutFile \"dist\\\\${{ env.APP_NAME }}-${{ env.APP_VERSION }}-Installer.exe\" | ||
| InstallDir \"\$PROGRAMFILES64\\\\\${{APP_NAME}}\" | ||
| InstallDirRegKey HKLM \"Software\\\\\${{APP_NAME}}\" \"Install_Dir\" | ||
| RequestExecutionLevel admin | ||
| ; Modern UI | ||
| !include \"MUI2.nsh\" | ||
| !define MUI_ABORTWARNING | ||
| ; Use custom icon if available | ||
| !if /FileExists \"${{ env.APP_NAME }}.ico\" | ||
| !define MUI_ICON \"${{ env.APP_NAME }}.ico\" | ||
| !define MUI_UNICON \"${{ env.APP_NAME }}.ico\" | ||
| !else | ||
| !define MUI_ICON \"\${{NSISDIR}}\\\\Contrib\\\\Graphics\\\\Icons\\\\modern-install.ico\" | ||
| !define MUI_UNICON \"\${{NSISDIR}}\\\\Contrib\\\\Graphics\\\\Icons\\\\modern-uninstall.ico\" | ||
| !endif | ||
| ; Pages | ||
| !insertmacro MUI_PAGE_WELCOME | ||
| !if /FileExists \"${{ env.LICENSE_FILE }}\" | ||
| !insertmacro MUI_PAGE_LICENSE \"${{ env.LICENSE_FILE }}\" | ||
| !endif | ||
| !insertmacro MUI_PAGE_DIRECTORY | ||
| !insertmacro MUI_PAGE_INSTFILES | ||
| !insertmacro MUI_PAGE_FINISH | ||
| ; Uninstaller pages | ||
| !insertmacro MUI_UNPAGE_WELCOME | ||
| !insertmacro MUI_UNPAGE_CONFIRM | ||
| !insertmacro MUI_UNPAGE_INSTFILES | ||
| !insertmacro MUI_UNPAGE_FINISH | ||
| ; Languages | ||
| !insertmacro MUI_LANGUAGE \"English\" | ||
| ; Installer sections | ||
| Section \"Install\" | ||
| SetOutPath \$INSTDIR | ||
| File \"dist\\\\${{ env.APP_NAME }}.exe\" | ||
| ; Create start menu shortcut | ||
| CreateDirectory \"\$SMPROGRAMS\\\\\${{APP_NAME}}\" | ||
| CreateShortcut \"\$SMPROGRAMS\\\\\${{APP_NAME}}\\\\\${{APP_NAME}}.lnk\" \"\$INSTDIR\\\\\${{APP_EXE}}\" | ||
| CreateShortcut \"\$SMPROGRAMS\\\\\${{APP_NAME}}\\\\Uninstall.lnk\" \"\$INSTDIR\\\\uninstall.exe\" | ||
| ; Create desktop shortcut | ||
| CreateShortcut \"\$DESKTOP\\\\\${{APP_NAME}}.lnk\" \"\$INSTDIR\\\\\${{APP_EXE}}\" | ||
| ; Write registry keys | ||
| WriteRegStr HKLM \"Software\\\\\${{APP_NAME}}\" \"Install_Dir\" \"\$INSTDIR\" | ||
| WriteRegStr HKLM \"Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Uninstall\\\\\${{APP_NAME}}\" \"DisplayName\" \"\${{APP_NAME}}\" | ||
| WriteRegStr HKLM \"Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Uninstall\\\\\${{APP_NAME}}\" \"UninstallString\" '\"\$INSTDIR\\\\uninstall.exe\"' | ||
| WriteRegStr HKLM \"Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Uninstall\\\\\${{APP_NAME}}\" \"DisplayVersion\" \"\${{APP_VERSION}}\" | ||
| WriteRegStr HKLM \"Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Uninstall\\\\\${{APP_NAME}}\" \"Publisher\" \"\${{APP_PUBLISHER}}\" | ||
| WriteRegStr HKLM \"Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Uninstall\\\\\${{APP_NAME}}\" \"URLInfoAbout\" \"\${{APP_URL}}\" | ||
| WriteRegDWORD HKLM \"Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Uninstall\\\\\${{APP_NAME}}\" \"NoModify\" 1 | ||
| WriteRegDWORD HKLM \"Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Uninstall\\\\\${{APP_NAME}}\" \"NoRepair\" 1 | ||
| ; Create uninstaller | ||
| WriteUninstaller \"\$INSTDIR\\\\uninstall.exe\" | ||
| SectionEnd | ||
| ; Uninstaller section | ||
| Section \"Uninstall\" | ||
| Delete \"\$INSTDIR\\\\\${{APP_EXE}}\" | ||
| Delete \"\$INSTDIR\\\\uninstall.exe\" | ||
| RMDir \"\$INSTDIR\" | ||
| Delete \"\$SMPROGRAMS\\\\\${{APP_NAME}}\\\\\${{APP_NAME}}.lnk\" | ||
| Delete \"\$SMPROGRAMS\\\\\${{APP_NAME}}\\\\Uninstall.lnk\" | ||
| RMDir \"\$SMPROGRAMS\\\\\${{APP_NAME}}\" | ||
| Delete \"\$DESKTOP\\\\\${{APP_NAME}}.lnk\" | ||
| DeleteRegKey HKLM \"Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Uninstall\\\\\${{APP_NAME}}\" | ||
| DeleteRegKey HKLM \"Software\\\\\${{APP_NAME}}\" | ||
| SectionEnd | ||
| ''' | ||
| with open('installer.nsi', 'w', encoding='utf-8') as f: | ||
| f.write(nsis_content) | ||
| print('NSIS installer script created successfully') | ||
| " | ||
| shell: bash | ||
| # Create default license if needed | ||
| - name: Create default license file (Windows only) | ||
| if: runner.os == 'Windows' | ||
| run: | | ||
| python -c " | ||
| import os | ||
| license_file = '${{ env.LICENSE_FILE }}' | ||
| if not os.path.exists(license_file): | ||
| license_text = '''MIT License | ||
| Copyright (c) $(date +%Y) ${{ env.APP_AUTHOR }} | ||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| of this software and associated documentation files (the \"Software\"), to deal | ||
| in the Software without restriction, including without limitation the rights | ||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| copies of the Software, and to permit persons to whom the Software is | ||
| furnished to do so, subject to the following conditions: | ||
| The above copyright notice and this permission notice shall be included in all | ||
| copies or substantial portions of the Software. | ||
| THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| SOFTWARE.''' | ||
| with open(license_file, 'w', encoding='utf-8') as f: | ||
| f.write(license_text) | ||
| print(f'{license_file} created') | ||
| else: | ||
| print(f'{license_file} already exists') | ||
| " | ||
| shell: bash | ||
| # Build Windows installer | ||
| - name: Build Windows installer | ||
| if: runner.os == 'Windows' | ||
| run: makensis installer.nsi | ||
| - name: Set build timestamp | ||
| run: echo "DATE_TAG=$(date +'%Y-%m-%d-%H%M')" >> $GITHUB_ENV | ||
| shell: bash | ||
| - name: Upload artifact | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: ${{ env.APP_NAME }}-${{ matrix.os }}-${{ env.DATE_TAG }} | ||
| path: | | ||
| dist/${{ env.APP_NAME }}-${{ env.APP_VERSION }}.dmg | ||
| dist/${{ env.APP_NAME }}-${{ env.APP_VERSION }}-Installer.exe | ||
| dist/${{ env.APP_NAME }}.AppImage | ||
| dist/${{ env.APP_NAME }}-linux.tar.gz | ||
| dist/${{ env.APP_NAME }} | ||
| dist/${{ env.APP_NAME }}.app | ||