Skip to content

example placeholder logo #1

example placeholder logo

example placeholder logo #1

Workflow file for this run

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

View workflow run for this annotation

GitHub Actions / Build Binaries

Invalid workflow file

The workflow is not valid. .github/workflows/build.yml (Line: 310, Col: 14): Unrecognized named-value: 'APP_NAME'. Located at position 1 within expression: APP_NAME
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