-
Notifications
You must be signed in to change notification settings - Fork 0
463 lines (397 loc) · 18.5 KB
/
build.yml
File metadata and controls
463 lines (397 loc) · 18.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
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: |
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