Skip to content

Commit e95f122

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 49fc4a1 commit e95f122

8 files changed

Lines changed: 264 additions & 28 deletions

File tree

.github/workflows/release.yml

Lines changed: 153 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,179 @@ 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+
defaults:
84+
run:
85+
shell: msys2 {0}
86+
87+
steps:
88+
- name: Checkout repository
89+
uses: actions/checkout@v6
90+
91+
- name: Setup MSYS2
92+
uses: msys2/setup-msys2@v2
93+
with:
94+
msystem: UCRT64
95+
cache: true
96+
install: >-
97+
mingw-w64-ucrt-x86_64-gtk4
98+
mingw-w64-ucrt-x86_64-libadwaita
99+
mingw-w64-ucrt-x86_64-python
100+
mingw-w64-ucrt-x86_64-python-gobject
101+
mingw-w64-ucrt-x86_64-python-pip
102+
mingw-w64-ucrt-x86_64-glib2
103+
mingw-w64-ucrt-x86_64-pyinstaller
104+
mingw-w64-ucrt-x86_64-pyinstaller-hooks-contrib
105+
mingw-w64-ucrt-x86_64-adwaita-icon-theme
106+
mingw-w64-ucrt-x86_64-imagemagick
107+
108+
- name: Convert icon to .ico
109+
run: |
110+
magick -background none data/icons/hicolor/scalable/apps/co.logonoff.awakeonlan.svg \
111+
-define icon:auto-resize=256,128,64,48,32,16 \
112+
awakeonlan.ico
113+
114+
- name: Prepare entry point
115+
run: |
116+
VERSION=${{ needs.release.outputs.version }}
117+
sed -i "s|@PYTHON@|/usr/bin/python3|" src/awakeonlan.in
118+
sed -i "s|@VERSION@|$VERSION|" src/awakeonlan.in
119+
sed -i "s|@pkgdatadir@|.|" src/awakeonlan.in
120+
sed -i "s|@localedir@|./locale|" src/awakeonlan.in
121+
122+
- name: Compile GResources
123+
run: |
124+
cp data/co.logonoff.awakeonlan.metainfo.xml.in data/co.logonoff.awakeonlan.metainfo.xml
125+
cd src
126+
glib-compile-resources --sourcedir=. --sourcedir=.. awakeonlan.gresource.xml --target=awakeonlan.gresource
127+
128+
- name: Compile GSettings schemas
129+
run: glib-compile-schemas data/
130+
131+
- name: Build with PyInstaller
132+
run: pyinstaller awakeonlan.spec
133+
134+
- name: Bundle additional resources
135+
run: |
136+
MSYS_ROOT="/$MSYSTEM"
137+
138+
# Win32 typelibs into PyInstaller's typelib directory
139+
cp $MSYS_ROOT/lib/girepository-1.0/GioWin32-*.typelib dist/awakeonlan/_internal/gi_typelibs/
140+
cp $MSYS_ROOT/lib/girepository-1.0/GLibWin32-*.typelib dist/awakeonlan/_internal/gi_typelibs/
141+
cp $MSYS_ROOT/lib/girepository-1.0/GdkWin32-*.typelib dist/awakeonlan/_internal/gi_typelibs/
142+
143+
# GSettings schemas
144+
mkdir -p dist/awakeonlan/share/glib-2.0/schemas
145+
cp $MSYS_ROOT/share/glib-2.0/schemas/gschemas.compiled dist/awakeonlan/share/glib-2.0/schemas/ || true
146+
cp data/gschemas.compiled dist/awakeonlan/share/glib-2.0/schemas/ 2>/dev/null || true
147+
148+
# Icon themes
149+
mkdir -p dist/awakeonlan/share/icons
150+
cp -r $MSYS_ROOT/share/icons/Adwaita dist/awakeonlan/share/icons/ || true
151+
cp -r $MSYS_ROOT/share/icons/hicolor dist/awakeonlan/share/icons/ || true
152+
153+
# GTK4 theme
154+
mkdir -p dist/awakeonlan/share/gtk-4.0
155+
cp -r $MSYS_ROOT/share/gtk-4.0/gtk.css dist/awakeonlan/share/gtk-4.0/ || true
156+
157+
- name: Create ZIP archive
158+
shell: pwsh
159+
run: |
160+
Compress-Archive -Path dist/awakeonlan -DestinationPath awakeonlan-${{ needs.release.outputs.version }}-windows-x86_64.zip
161+
162+
- name: Calculate SHA256
163+
id: sha256
164+
shell: pwsh
77165
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)
166+
$hash = (Get-FileHash awakeonlan-${{ needs.release.outputs.version }}-windows-x86_64.zip -Algorithm SHA256).Hash.ToLower()
167+
echo "hash=$hash" >> $env:GITHUB_OUTPUT
168+
169+
- name: Upload to release
170+
shell: pwsh
171+
env:
172+
GH_TOKEN: ${{ github.token }}
173+
run: |
174+
gh release upload ${{ needs.release.outputs.version }} awakeonlan-${{ needs.release.outputs.version }}-windows-x86_64.zip
175+
176+
update-manifests:
177+
runs-on: ubuntu-latest
178+
needs: [release, windows-build]
179+
permissions:
180+
contents: write
181+
182+
steps:
183+
- name: Checkout repository
184+
uses: actions/checkout@v6
185+
186+
- name: Update manifests
187+
env:
188+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
189+
run: |
190+
VERSION=${{ needs.release.outputs.version }}
191+
TARBALL_HASH=${{ needs.release.outputs.tarball-hash }}
192+
WINDOWS_HASH=${{ needs.windows-build.outputs.hash }}
193+
194+
# Update Homebrew formula
195+
sed -i "s/version \".*\"/version \"$VERSION\"/" Formula/awakeonlan.rb
196+
sed -i "s/sha256 \".*\"/sha256 \"$TARBALL_HASH\"/" Formula/awakeonlan.rb
197+
198+
# Update Scoop manifest
199+
sed -i "s/\"version\": \".*\"/\"version\": \"$VERSION\"/" bucket/awakeonlan.json
200+
sed -i "s|download/.*/awakeonlan-.*-windows-x86_64.zip|download/${VERSION}/awakeonlan-${VERSION}-windows-x86_64.zip|" bucket/awakeonlan.json
201+
sed -i "s/\"hash\": \".*\"/\"hash\": \"$WINDOWS_HASH\"/" bucket/awakeonlan.json
202+
203+
FORMULA_CONTENT=$(base64 -w 0 < Formula/awakeonlan.rb)
204+
SCOOP_CONTENT=$(base64 -w 0 < bucket/awakeonlan.json)
81205
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')
206+
82207
jq -n \
83208
--arg oid "$MAIN_OID" \
84-
--arg content "$CONTENT" \
85-
--arg version "${{ steps.version.outputs.version }}" \
209+
--arg formula "$FORMULA_CONTENT" \
210+
--arg scoop "$SCOOP_CONTENT" \
211+
--arg version "$VERSION" \
86212
'{
87213
query: "mutation($input: CreateCommitOnBranchInput!) { createCommitOnBranch(input: $input) { commit { oid } } }",
88214
variables: {
89215
input: {
90216
branch: { repositoryNameWithOwner: "logonoff/awake-on-lan", branchName: "main" },
91217
expectedHeadOid: $oid,
92-
message: { headline: ("chore: update formula to " + $version) },
93-
fileChanges: { additions: [{ path: "Formula/awakeonlan.rb", contents: $content }] }
218+
message: { headline: ("chore: update manifests to " + $version) },
219+
fileChanges: { additions: [
220+
{ path: "Formula/awakeonlan.rb", contents: $formula },
221+
{ path: "bucket/awakeonlan.json", contents: $scoop }
222+
] }
94223
}
95224
}
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 }}
225+
}'
226+
# | gh api graphql --input -
101227
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 }}
228+
# - name: Sync homebrew-bucket
229+
# run: |
230+
# gh api repos/logonoff/homebrew-bucket/dispatches \
231+
# -f event_type=sync
232+
# env:
233+
# GH_TOKEN: ${{ secrets.BUCKET_PAT }}
108234

109235
- name: Check if next branch can be fast-forwarded
110236
id: check-next
237+
env:
238+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
111239
run: |
112240
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
241+
git fetch origin next main
242+
if git merge-base --is-ancestor origin/next origin/main; then
115243
echo "can_ff=true" >> "$GITHUB_OUTPUT"
116244
fi
117245
fi
118246
119247
- name: Fast-forward next branch
120248
if: steps.check-next.outputs.can_ff == 'true'
121-
run: git push origin HEAD:next
249+
run: git push origin origin/main:next
122250
env:
123251
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: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import os
2+
import sys
3+
4+
if getattr(sys, 'frozen', False):
5+
exe_dir = os.path.dirname(sys.executable)
6+
meipass = sys._MEIPASS
7+
schema_dirs = os.pathsep.join([
8+
os.path.join(exe_dir, 'share', 'glib-2.0', 'schemas'),
9+
os.path.join(meipass, 'share', 'glib-2.0', 'schemas'),
10+
])
11+
os.environ['GSETTINGS_SCHEMA_DIR'] = schema_dirs
12+
os.environ['XDG_DATA_DIRS'] = os.path.join(exe_dir, 'share')
13+
os.environ['GSK_RENDERER'] = 'cairo'

awakeonlan.spec

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
a = Analysis(
2+
['src/awakeonlan.in'],
3+
datas=[
4+
('src/awakeonlan.gresource', '.'),
5+
('src/__init__.py', 'awakeonlan'),
6+
('src/main.py', 'awakeonlan'),
7+
('src/window.py', 'awakeonlan'),
8+
('src/add_dialog.py', 'awakeonlan'),
9+
('src/wol_client.py', 'awakeonlan'),
10+
('src/settings_manager.py', 'awakeonlan'),
11+
('data/co.logonoff.awakeonlan.gschema.xml', 'share/glib-2.0/schemas'),
12+
('data/gschemas.compiled', 'share/glib-2.0/schemas'),
13+
],
14+
hiddenimports=[
15+
'gi',
16+
'gi.repository.Gtk',
17+
'gi.repository.Adw',
18+
'gi.repository.Gio',
19+
'gi.repository.GLib',
20+
'gi.repository.GObject',
21+
'gi.repository.Pango',
22+
'gi.repository.GdkPixbuf',
23+
'gi.repository.Gdk',
24+
'gi.repository.Graphene',
25+
'awakeonlan',
26+
'awakeonlan.main',
27+
'awakeonlan.window',
28+
'awakeonlan.add_dialog',
29+
'awakeonlan.wol_client',
30+
'awakeonlan.settings_manager',
31+
],
32+
hooksconfig={
33+
'gi': {
34+
'module-versions': {
35+
'Gtk': '4.0',
36+
'Adw': '1',
37+
},
38+
},
39+
},
40+
runtime_hooks=['_packaging/runtime_hook.py'],
41+
)
42+
43+
pyz = PYZ(a.pure)
44+
45+
exe = EXE(
46+
pyz,
47+
a.scripts,
48+
[],
49+
exclude_binaries=True,
50+
name='awakeonlan',
51+
console=True,
52+
icon='awakeonlan.ico',
53+
)
54+
55+
coll = COLLECT(
56+
exe,
57+
a.binaries,
58+
a.datas,
59+
name='awakeonlan',
60+
)

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: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,11 @@ class awakeonlanApplication(Adw.Application):
3232
version: str
3333

3434
def __init__(self, version: str = '0.0.0'):
35+
flags = Gio.ApplicationFlags.DEFAULT_FLAGS
36+
if sys.platform == 'win32':
37+
flags |= Gio.ApplicationFlags.NON_UNIQUE
3538
super().__init__(application_id='co.logonoff.awakeonlan',
36-
flags=Gio.ApplicationFlags.DEFAULT_FLAGS)
39+
flags=flags)
3740
self.version = version
3841
self.create_action('quit', lambda *_: self.quit(), ['<primary>q', '<primary>w'])
3942
self.create_action('about', self.on_about_action)

src/window.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
# SPDX-License-Identifier: GPL-3.0-or-later
1919

2020
import os
21+
import sys
2122
from gi.repository import Adw
2223
from gi.repository import Gtk
2324
from .add_dialog import AddDialogBox
@@ -41,9 +42,12 @@ def __init__(self, **kwargs):
4142

4243
self.set_title(_('Awake on LAN'))
4344

44-
XDG_CONFIG_HOME = os.getenv('XDG_CONFIG_HOME', os.path.expanduser('~/.config'))
45+
if sys.platform == 'win32':
46+
config_home = os.getenv('APPDATA', os.path.expanduser('~'))
47+
else:
48+
config_home = os.getenv('XDG_CONFIG_HOME', os.path.expanduser('~/.config'))
4549

46-
self.wol_clients = SettingsManager(base_path=XDG_CONFIG_HOME, version=self.get_application().version)
50+
self.wol_clients = SettingsManager(base_path=config_home, version=self.get_application().version)
4751
self.wol_clients.load_settings()
4852

4953
self.get_application().create_action(

0 commit comments

Comments
 (0)