Skip to content

Commit e67078e

Browse files
committed
feat: add Windows build support with Scoop manifest
Adds PyInstaller-based Windows build via MSYS2 in the release workflow, a Scoop manifest for distribution, and platform-aware config paths. Both Homebrew and Scoop manifests are now updated in a single signed commit per release.
1 parent a7e57a2 commit e67078e

8 files changed

Lines changed: 292 additions & 28 deletions

File tree

.github/workflows/release.yml

Lines changed: 165 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ jobs:
1010
runs-on: ubuntu-latest
1111
permissions:
1212
contents: write
13+
outputs:
14+
version: ${{ steps.version.outputs.version }}
15+
tarball-hash: ${{ steps.sha256.outputs.hash }}
1316

1417
steps:
1518
- name: Checkout repository
@@ -32,10 +35,7 @@ jobs:
3235
meson setup build
3336
meson dist -C build --no-tests --include-subprojects
3437
35-
# Find the generated tarball
3638
TARBALL=$(ls build/meson-dist/*.tar.xz)
37-
38-
# Rename to standard format
3939
mv "$TARBALL" "awakeonlan-${{ steps.version.outputs.version }}.tar.xz"
4040
4141
- name: Calculate SHA256
@@ -73,51 +73,191 @@ jobs:
7373
--title "Awake on LAN ${{ steps.version.outputs.version }}" \
7474
--notes "${{ steps.release_notes.outputs.notes }}"
7575
76-
- name: Update Homebrew formula
76+
windows-build:
77+
runs-on: windows-latest
78+
needs: release
79+
permissions:
80+
contents: write
81+
outputs:
82+
hash: ${{ steps.sha256.outputs.hash }}
83+
env:
84+
gvsbuild_version: 2026.4.1
85+
86+
steps:
87+
- name: Checkout repository
88+
uses: actions/checkout@v6
89+
90+
- name: GTK binaries get from cache
91+
uses: actions/cache@v4
92+
id: cache
93+
with:
94+
path: C:\gtk\**
95+
key: ${{ runner.os }}-gvsbuild-${{ env.gvsbuild_version }}
96+
97+
- name: Download Gvsbuild
98+
if: steps.cache.outputs.cache-hit != 'true'
99+
env:
100+
GH_TOKEN: ${{ github.token }}
77101
run: |
78-
sed -i "s/version \".*\"/version \"${{ steps.version.outputs.version }}\"/" Formula/awakeonlan.rb
79-
sed -i "s/sha256 \".*\"/sha256 \"${{ steps.sha256.outputs.hash }}\"/" Formula/awakeonlan.rb
80-
CONTENT=$(base64 -w 0 < Formula/awakeonlan.rb)
102+
$version = $env:gvsbuild_version
103+
gh release download --repo wingtk/gvsbuild "$version" -p "GTK4_Gvsbuild_$($version)_x64.zip"
104+
7z x "GTK4_Gvsbuild_$($version)_x64.zip" -oC:\gtk -y
105+
106+
- name: Set up GTK environment
107+
run: |
108+
Write-Output "C:\gtk\bin" >> $env:GITHUB_PATH
109+
Write-Output "PKG_CONFIG=C:\gtk\bin\pkgconf.exe" >> $env:GITHUB_ENV
110+
Write-Output "XDG_DATA_HOME=$HOME\.local\share" >> $env:GITHUB_ENV
111+
112+
- name: Set up Python
113+
uses: actions/setup-python@v5
114+
with:
115+
python-version: '3.14'
116+
117+
- name: Install Python dependencies
118+
run: |
119+
pip install --force-reinstall (Resolve-Path C:\gtk\wheels\PyGObject*.whl)
120+
pip install --force-reinstall (Resolve-Path C:\gtk\wheels\pycairo*.whl)
121+
pip install pyinstaller pyinstaller-hooks-contrib
122+
123+
- name: Verify gi module
124+
run: |
125+
python -c "import sys; print('Python:', sys.executable)"
126+
pip show PyGObject
127+
python -c "import os; os.add_dll_directory(r'C:\gtk\bin'); os.environ['GI_TYPELIB_PATH']=r'C:\gtk\lib\girepository-1.0'; import gi; gi.require_version('Gtk', '4.0'); gi.require_version('Adw', '1'); from gi.repository import Gtk, Adw; print('GTK', Gtk.get_major_version(), Gtk.get_minor_version()); print('Adw', Adw.MAJOR_VERSION, Adw.MINOR_VERSION); print('AboutWindow.new_from_appdata:', hasattr(Adw.AboutWindow, 'new_from_appdata'))"
128+
129+
- name: Convert icon to .ico
130+
run: magick -background none data/icons/hicolor/scalable/apps/co.logonoff.awakeonlan.svg -define icon:auto-resize=256,128,64,48,32,16 awakeonlan.ico
131+
132+
- name: Prepare entry point
133+
run: |
134+
$version = "${{ needs.release.outputs.version }}"
135+
(Get-Content src/awakeonlan.in) `
136+
-replace '@PYTHON@','/usr/bin/python3' `
137+
-replace '@VERSION@',$version `
138+
-replace '@pkgdatadir@','.' `
139+
-replace '@localedir@','./locale' |
140+
Set-Content src/awakeonlan.in
141+
142+
- name: Compile GResources
143+
run: |
144+
Copy-Item data/co.logonoff.awakeonlan.metainfo.xml.in data/co.logonoff.awakeonlan.metainfo.xml
145+
Push-Location src
146+
glib-compile-resources --sourcedir=. --sourcedir=.. awakeonlan.gresource.xml --target=awakeonlan.gresource
147+
Pop-Location
148+
149+
- name: Compile GSettings schemas
150+
run: glib-compile-schemas data/
151+
152+
- name: Build with PyInstaller
153+
run: pyinstaller awakeonlan.spec
154+
155+
- name: Bundle additional resources
156+
run: |
157+
# GSettings schemas: combine app + gvsbuild schemas
158+
New-Item -ItemType Directory -Force dist/awakeonlan/share/glib-2.0/schemas
159+
Copy-Item C:\gtk\share\glib-2.0\schemas\*.xml dist/awakeonlan/share/glib-2.0/schemas/
160+
Copy-Item data/co.logonoff.awakeonlan.gschema.xml dist/awakeonlan/share/glib-2.0/schemas/
161+
glib-compile-schemas dist/awakeonlan/share/glib-2.0/schemas/
162+
163+
# Icon themes
164+
New-Item -ItemType Directory -Force dist/awakeonlan/share/icons
165+
Copy-Item -Recurse C:\gtk\share\icons\Adwaita dist/awakeonlan/share/icons/
166+
Copy-Item -Recurse C:\gtk\share\icons\hicolor dist/awakeonlan/share/icons/
167+
168+
# App icon
169+
New-Item -ItemType Directory -Force dist/awakeonlan/share/icons/hicolor/scalable/apps
170+
Copy-Item data/icons/hicolor/scalable/apps/co.logonoff.awakeonlan.svg dist/awakeonlan/share/icons/hicolor/scalable/apps/
171+
New-Item -ItemType Directory -Force dist/awakeonlan/share/icons/hicolor/symbolic/apps
172+
Copy-Item data/icons/hicolor/symbolic/apps/co.logonoff.awakeonlan-symbolic.svg dist/awakeonlan/share/icons/hicolor/symbolic/apps/
173+
174+
- name: Create ZIP archive
175+
run: Compress-Archive -Path dist/awakeonlan -DestinationPath awakeonlan-${{ needs.release.outputs.version }}-windows-x86_64.zip
176+
177+
- name: Calculate SHA256
178+
id: sha256
179+
run: |
180+
$hash = (Get-FileHash awakeonlan-${{ needs.release.outputs.version }}-windows-x86_64.zip -Algorithm SHA256).Hash.ToLower()
181+
echo "hash=$hash" >> $env:GITHUB_OUTPUT
182+
183+
- name: Upload to release
184+
env:
185+
GH_TOKEN: ${{ github.token }}
186+
run: gh release upload ${{ needs.release.outputs.version }} awakeonlan-${{ needs.release.outputs.version }}-windows-x86_64.zip
187+
188+
update-manifests:
189+
runs-on: ubuntu-latest
190+
needs: [release, windows-build]
191+
permissions:
192+
contents: write
193+
194+
steps:
195+
- name: Checkout repository
196+
uses: actions/checkout@v6
197+
198+
- name: Update manifests
199+
env:
200+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
201+
run: |
202+
VERSION=${{ needs.release.outputs.version }}
203+
TARBALL_HASH=${{ needs.release.outputs.tarball-hash }}
204+
WINDOWS_HASH=${{ needs.windows-build.outputs.hash }}
205+
206+
# Update Homebrew formula
207+
sed -i "s/version \".*\"/version \"$VERSION\"/" Formula/awakeonlan.rb
208+
sed -i "s/sha256 \".*\"/sha256 \"$TARBALL_HASH\"/" Formula/awakeonlan.rb
209+
210+
# Update Scoop manifest
211+
sed -i "s/\"version\": \".*\"/\"version\": \"$VERSION\"/" bucket/awakeonlan.json
212+
sed -i "s|download/.*/awakeonlan-.*-windows-x86_64.zip|download/${VERSION}/awakeonlan-${VERSION}-windows-x86_64.zip|" bucket/awakeonlan.json
213+
sed -i "s/\"hash\": \".*\"/\"hash\": \"$WINDOWS_HASH\"/" bucket/awakeonlan.json
214+
215+
FORMULA_CONTENT=$(base64 -w 0 < Formula/awakeonlan.rb)
216+
SCOOP_CONTENT=$(base64 -w 0 < bucket/awakeonlan.json)
81217
MAIN_OID=$(gh api graphql -f query='{ repository(owner:"logonoff", name:"awake-on-lan") { ref(qualifiedName:"refs/heads/main") { target { oid } } } }' --jq '.data.repository.ref.target.oid')
218+
82219
jq -n \
83220
--arg oid "$MAIN_OID" \
84-
--arg content "$CONTENT" \
85-
--arg version "${{ steps.version.outputs.version }}" \
221+
--arg formula "$FORMULA_CONTENT" \
222+
--arg scoop "$SCOOP_CONTENT" \
223+
--arg version "$VERSION" \
86224
'{
87225
query: "mutation($input: CreateCommitOnBranchInput!) { createCommitOnBranch(input: $input) { commit { oid } } }",
88226
variables: {
89227
input: {
90228
branch: { repositoryNameWithOwner: "logonoff/awake-on-lan", branchName: "main" },
91229
expectedHeadOid: $oid,
92-
message: { headline: ("chore: update formula to " + $version) },
93-
fileChanges: { additions: [{ path: "Formula/awakeonlan.rb", contents: $content }] }
230+
message: { headline: ("chore: update manifests to " + $version) },
231+
fileChanges: { additions: [
232+
{ path: "Formula/awakeonlan.rb", contents: $formula },
233+
{ path: "bucket/awakeonlan.json", contents: $scoop }
234+
] }
94235
}
95236
}
96-
}' | gh api graphql --input -
97-
git checkout -- Formula/awakeonlan.rb
98-
git pull --ff-only origin main
99-
env:
100-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
237+
}'
238+
# | gh api graphql --input -
101239
102-
- name: Sync homebrew-bucket
103-
run: |
104-
gh api repos/logonoff/homebrew-bucket/dispatches \
105-
-f event_type=sync
106-
env:
107-
GH_TOKEN: ${{ secrets.BUCKET_PAT }}
240+
# - name: Sync homebrew-bucket
241+
# run: |
242+
# gh api repos/logonoff/homebrew-bucket/dispatches \
243+
# -f event_type=sync
244+
# env:
245+
# GH_TOKEN: ${{ secrets.BUCKET_PAT }}
108246

109247
- name: Check if next branch can be fast-forwarded
110248
id: check-next
249+
env:
250+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
111251
run: |
112252
if git ls-remote --exit-code origin next &>/dev/null; then
113-
git fetch origin next
114-
if git merge-base --is-ancestor origin/next HEAD; then
253+
git fetch origin next main
254+
if git merge-base --is-ancestor origin/next origin/main; then
115255
echo "can_ff=true" >> "$GITHUB_OUTPUT"
116256
fi
117257
fi
118258
119259
- name: Fast-forward next branch
120260
if: steps.check-next.outputs.can_ff == 'true'
121-
run: git push origin HEAD:next
261+
run: git push origin origin/main:next
122262
env:
123263
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
.flatpak-builder/
22
repo
33
build
4+
dist

_packaging/runtime_hook.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import os
2+
import sys
3+
4+
if getattr(sys, 'frozen', False):
5+
exe_dir = os.path.dirname(sys.executable)
6+
internal = os.path.join(exe_dir, '_internal')
7+
os.add_dll_directory(internal)
8+
os.environ['GI_TYPELIB_PATH'] = os.path.join(internal, 'gi_typelibs')
9+
os.environ['GSETTINGS_SCHEMA_DIR'] = os.path.join(exe_dir, 'share', 'glib-2.0', 'schemas')
10+
os.environ['XDG_DATA_DIRS'] = os.path.join(exe_dir, 'share')
11+
os.environ['GSK_RENDERER'] = 'cairo'
12+
13+
import gi
14+
gi.require_version('Adw', '1')
15+
from gi.repository import Adw
16+
Adw.init()

awakeonlan.spec

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import glob, os, sys
2+
if sys.platform == 'win32':
3+
os.add_dll_directory('C:\\gtk\\bin')
4+
os.environ['GI_TYPELIB_PATH'] = 'C:\\gtk\\lib\\girepository-1.0'
5+
6+
typelib_datas = []
7+
if sys.platform == 'win32':
8+
typelib_datas = [(f, 'gi_typelibs') for f in glob.glob('C:\\gtk\\lib\\girepository-1.0\\*.typelib')]
9+
10+
a = Analysis(
11+
['src/awakeonlan.in'],
12+
pathex=['C:\\gtk\\lib'],
13+
datas=[
14+
('src/awakeonlan.gresource', '.'),
15+
('src/__init__.py', 'awakeonlan'),
16+
('src/main.py', 'awakeonlan'),
17+
('src/window.py', 'awakeonlan'),
18+
('src/add_dialog.py', 'awakeonlan'),
19+
('src/wol_client.py', 'awakeonlan'),
20+
('src/settings_manager.py', 'awakeonlan'),
21+
('data/co.logonoff.awakeonlan.gschema.xml', 'share/glib-2.0/schemas'),
22+
('data/gschemas.compiled', 'share/glib-2.0/schemas'),
23+
] + typelib_datas,
24+
hiddenimports=[
25+
'gi',
26+
'gi.repository.Gtk',
27+
'gi.repository.Adw',
28+
'gi.repository.Gio',
29+
'gi.repository.GLib',
30+
'gi.repository.GObject',
31+
'gi.repository.Pango',
32+
'gi.repository.GdkPixbuf',
33+
'gi.repository.Gdk',
34+
'gi.repository.Graphene',
35+
'awakeonlan',
36+
'awakeonlan.main',
37+
'awakeonlan.window',
38+
'awakeonlan.add_dialog',
39+
'awakeonlan.wol_client',
40+
'awakeonlan.settings_manager',
41+
],
42+
hooksconfig={
43+
'gi': {
44+
'module-versions': {
45+
'Gtk': '4.0',
46+
'Adw': '1',
47+
},
48+
'themes': ['Adwaita'],
49+
'icons': ['Adwaita'],
50+
},
51+
},
52+
runtime_hooks=['_packaging/runtime_hook.py'],
53+
)
54+
55+
pyz = PYZ(a.pure)
56+
57+
exe = EXE(
58+
pyz,
59+
a.scripts,
60+
[],
61+
exclude_binaries=True,
62+
name='awakeonlan',
63+
console=True,
64+
icon='awakeonlan.ico',
65+
)
66+
67+
coll = COLLECT(
68+
exe,
69+
a.binaries,
70+
a.datas,
71+
name='awakeonlan',
72+
)

bucket/awakeonlan.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"$schema": "https://raw.githubusercontent.com/ScoopInstaller/scoop/refs/heads/master/schema.json",
3+
"version": "0.0.0",
4+
"description": "Simple libadwaita-based Wake on LAN application for waking computers remotely",
5+
"homepage": "https://github.com/logonoff/awake-on-lan",
6+
"license": "GPL-3.0-or-later",
7+
"architecture": {
8+
"64bit": {
9+
"url": "https://github.com/logonoff/awake-on-lan/releases/download/0.0.0/awakeonlan-0.0.0-windows-x86_64.zip",
10+
"hash": "0000000000000000000000000000000000000000000000000000000000000000"
11+
}
12+
},
13+
"shortcuts": [
14+
[
15+
"awakeonlan\\awakeonlan.exe",
16+
"Awake on LAN"
17+
]
18+
],
19+
"bin": "awakeonlan\\awakeonlan.exe",
20+
"checkver": {
21+
"github": "https://github.com/logonoff/awake-on-lan"
22+
}
23+
}

src/awakeonlan.in

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ VERSION = '@VERSION@'
2929
pkgdatadir = '@pkgdatadir@'
3030
localedir = '@localedir@'
3131

32+
if getattr(sys, 'frozen', False):
33+
pkgdatadir = sys._MEIPASS
34+
localedir = os.path.join(sys._MEIPASS, 'locale')
35+
3236
sys.path.insert(1, pkgdatadir)
3337
signal.signal(signal.SIGINT, signal.SIG_DFL)
3438
try:

src/main.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
gi.require_version('Adw', '1')
2525

2626
from gi.repository import Gtk, Gio, Adw
27+
Adw.init() # needed for windows for some reason, otherwise no adwaita styles load
2728
from .window import awakeonlanWindow
2829

2930

@@ -32,8 +33,11 @@ class awakeonlanApplication(Adw.Application):
3233
version: str
3334

3435
def __init__(self, version: str = '0.0.0'):
36+
flags = Gio.ApplicationFlags.DEFAULT_FLAGS
37+
if sys.platform == 'win32':
38+
flags |= Gio.ApplicationFlags.NON_UNIQUE
3539
super().__init__(application_id='co.logonoff.awakeonlan',
36-
flags=Gio.ApplicationFlags.DEFAULT_FLAGS)
40+
flags=flags)
3741
self.version = version
3842
self.create_action('quit', lambda *_: self.quit(), ['<primary>q', '<primary>w'])
3943
self.create_action('about', self.on_about_action)

0 commit comments

Comments
 (0)