From 5a382b2b6a64d26ba8d0ed294e5eaf6a6a7ec994 Mon Sep 17 00:00:00 2001 From: Christophe Monniez Date: Fri, 3 Apr 2026 10:42:00 +0200 Subject: [PATCH 1/9] [IMP] pyinstaller: skip aw-qt and python server When building windows package, we exclude aw-qt and the python server from the Windows installer. This is done through the environment variable ODOO_WINDOWS_BUILD --- Makefile | 22 ++++++++++++++++++---- aw.spec | 53 ++++++++++++++++++++++++++++++++++------------------- 2 files changed, 52 insertions(+), 23 deletions(-) diff --git a/Makefile b/Makefile index 9b2a5c23c..9c623d59a 100644 --- a/Makefile +++ b/Makefile @@ -27,6 +27,14 @@ endif ifeq ($(SKIP_SERVER_RUST),true) SUBMODULES := $(filter-out aw-server-rust,$(SUBMODULES)) endif +# Exclude aw-server (Python) when using aw-server-rust only +ifeq ($(SKIP_SERVER_PYTHON),true) + SUBMODULES := $(filter-out aw-server,$(SUBMODULES)) +endif +# Odoo-specific Windows build: replaces aw-qt with aw-systray-odoo +ifeq ($(ODOO_WINDOWS_BUILD),true) + SUBMODULES := $(filter-out aw-qt,$(SUBMODULES)) +endif # Include extras if AW_EXTRAS is true ifeq ($(AW_EXTRAS),true) SUBMODULES := $(SUBMODULES) aw-notify aw-watcher-input @@ -79,7 +87,9 @@ build: aw-core/.git make --directory=aw-client build make --directory=aw-core build # Needed to ensure that the server has the correct version set +ifneq ($(SKIP_SERVER_PYTHON),true) python -c "import aw_server; print(aw_server.__version__)" +endif # Install @@ -194,10 +204,14 @@ ifeq ($(TAURI_BUILD),true) mkdir -p dist/activitywatch/aw-server-rust cp aw-server-rust/target/$(targetdir)/aw-sync dist/activitywatch/aw-server-rust/aw-sync else -# Move aw-qt to the root of the dist folder - mv dist/activitywatch/aw-qt aw-qt-tmp - mv aw-qt-tmp/* dist/activitywatch - rmdir aw-qt-tmp +ifeq ($(ODOO_WINDOWS_BUILD),true) +# ODOO_WINDOWS_BUILD: Install pystray and build aw-systray-odoo.exe via PyInstaller + @echo "ODOO_WINDOWS_BUILD: Installing pystray..." + python -m pip install pystray pillow pywin32 + @echo "ODOO_WINDOWS_BUILD: Building aw-systray-odoo.exe via PyInstaller..." + pyinstaller --clean --noconfirm odoo-setup/aw-systray-odoo.spec + cp dist/aw-systray-odoo.exe dist/activitywatch/aw-systray-odoo.exe +endif endif # Remove problem-causing binaries rm -f dist/activitywatch/libdrm.so.2 # see: https://github.com/ActivityWatch/activitywatch/issues/161 diff --git a/aw.spec b/aw.spec index 70e2e5b19..60a633404 100644 --- a/aw.spec +++ b/aw.spec @@ -101,16 +101,25 @@ if not aw_server_rust_bin.exists(): skip_rust = True print("Skipping Rust build because aw-server-rust binary not found.") +skip_aw_server_python = os.environ.get("SKIP_SERVER_PYTHON", "false").lower() == "true" +if skip_aw_server_python: + print("Skipping aw-server (Python) packaging, using aw-server-rust only.") -aw_qt_a = build_analysis( - "aw-qt", - aw_qt_location, - binaries=[(aw_server_rust_bin, "."), (aw_sync_bin, ".")] if not skip_rust else [], - datas=[ - (aw_qt_location / "resources/aw-qt.desktop", "aw_qt/resources"), - (aw_qt_location / "media", "aw_qt/media"), - ], -) +skip_aw_qt = os.environ.get("ODOO_WINDOWS_BUILD", "false").lower() == "true" +if skip_aw_qt: + print("Skipping aw-qt packaging, using aw-systray-odoo instead.") + + +if not skip_aw_qt: + aw_qt_a = build_analysis( + "aw-qt", + aw_qt_location, + binaries=[(aw_server_rust_bin, "."), (aw_sync_bin, ".")] if not skip_rust else [], + datas=[ + (aw_qt_location / "resources/aw-qt.desktop", "aw_qt/resources"), + (aw_qt_location / "media", "aw_qt/media"), + ], + ) aw_server_a = build_analysis( "aw-server", aws_location, @@ -182,12 +191,14 @@ aw_notify_a = None if skip_aw_notify else build_analysis( # MERGE takes a bit weird arguments, it wants tuples which consists of # the analysis paired with the script name and the bin name merge_args = [ - (aw_server_a, "aw-server", "aw-server"), - (aw_qt_a, "aw-qt", "aw-qt"), (aw_watcher_afk_a, "aw-watcher-afk", "aw-watcher-afk"), (aw_watcher_window_a, "aw-watcher-window", "aw-watcher-window"), (aw_watcher_input_a, "aw-watcher-input", "aw-watcher-input"), ] +if not skip_aw_server_python: + merge_args.insert(0, (aw_server_a, "aw-server", "aw-server")) +if not skip_aw_qt: + merge_args.append((aw_qt_a, "aw-qt", "aw-qt")) if aw_notify_a is not None: merge_args.append((aw_notify_a, "aw-notify", "aw-notify")) @@ -195,7 +206,8 @@ MERGE(*merge_args) # aw-server -aws_coll = build_collect(aw_server_a, "aw-server") +if not skip_aw_server_python: + aws_coll = build_collect(aw_server_a, "aw-server") # aw-watcher-window aww_coll = build_collect(aw_watcher_window_a, "aw-watcher-window") @@ -204,11 +216,12 @@ aww_coll = build_collect(aw_watcher_window_a, "aw-watcher-window") awa_coll = build_collect(aw_watcher_afk_a, "aw-watcher-afk") # aw-qt -awq_coll = build_collect( - aw_qt_a, - "aw-qt", - console=False if platform.system() == "Windows" else True, -) +if not skip_aw_qt: + awq_coll = build_collect( + aw_qt_a, + "aw-qt", + console=False if platform.system() == "Windows" else True, + ) # aw-watcher-input awi_coll = build_collect(aw_watcher_input_a, "aw-watcher-input") @@ -218,12 +231,14 @@ aw_notify_coll = build_collect(aw_notify_a, "aw-notify") if aw_notify_a is not N if platform.system() == "Darwin": bundle_args = [ - awq_coll, - aws_coll, aww_coll, awa_coll, awi_coll, ] + if not skip_aw_server_python: + bundle_args.insert(1, aws_coll) + if not skip_aw_qt: + bundle_args.insert(0, awq_coll) if aw_notify_coll is not None: bundle_args.append(aw_notify_coll) From 6cae70dfd3bcc291db3fa9a3b1ff8391c8cfcffc Mon Sep 17 00:00:00 2001 From: Christophe Monniez Date: Fri, 3 Apr 2026 10:49:06 +0200 Subject: [PATCH 2/9] [IMP] packaging: adapt inno setup script --- scripts/package/activitywatch-setup.iss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/package/activitywatch-setup.iss b/scripts/package/activitywatch-setup.iss index dc582e597..459ff7e89 100644 --- a/scripts/package/activitywatch-setup.iss +++ b/scripts/package/activitywatch-setup.iss @@ -5,7 +5,7 @@ #define MyAppVersion GetEnv('AW_VERSION') #define MyAppPublisher "ActivityWatch Contributors" #define MyAppURL "https://activitywatch.net/" -#define MyAppExeName "aw-qt.exe" +#define MyAppExeName "aw-systray-odoo.exe" #define RootDir "..\.." #define DistDir "..\..\dist" @@ -45,7 +45,6 @@ Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{ Name: "StartMenuEntry" ; Description: "Start ActivityWatch when Windows starts"; GroupDescription: "Windows Startup"; MinVersion: 4,4; [Files] -Source: "{#DistDir}\activitywatch\aw-qt.exe"; DestDir: "{app}"; Flags: ignoreversion Source: "{#DistDir}\activitywatch\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs ; NOTE: Don't use "Flags: ignoreversion" on any shared system files From d41b1e48ee920e36c71a0e1f1b246a499fc5bc0b Mon Sep 17 00:00:00 2001 From: Christophe Monniez Date: Fri, 3 Apr 2026 10:50:51 +0200 Subject: [PATCH 3/9] [IMP] aw-systray-odoo: adapt for windows Adapt the script to work on Windows by using pystray instead of gi when building for Windows platform. While pystray is cross platform, we need to keep gi as python3-pystray package is not available on Ubuntu Jammy. So the script is now able to conditionaly use one or another. --- odoo-setup/aw-systray-odoo.py | 146 +++++++++++++++++++++++++--------- 1 file changed, 108 insertions(+), 38 deletions(-) diff --git a/odoo-setup/aw-systray-odoo.py b/odoo-setup/aw-systray-odoo.py index cea629011..328b68da8 100644 --- a/odoo-setup/aw-systray-odoo.py +++ b/odoo-setup/aw-systray-odoo.py @@ -1,34 +1,58 @@ -#!/usr/bin/python3 +#!/usr/bin/env python3 import os +import platform import subprocess import sys import tempfile import webbrowser -import gi -import psutil - -gi.require_version('Gtk', '3.0') -gi.require_version('AppIndicator3', '0.1') -from gi.repository import AppIndicator3, Gtk -from PIL import Image - -binaries = [ - '/opt/activitywatch/aw-server-rust/aw-server-rust', - '/opt/activitywatch/awatcher/aw-awatcher', -] - - -# generate an icon of an eye in the odoo purple collor -def get_icon_path(): +IS_WINDOWS = platform.system() == "Windows" + +if IS_WINDOWS: + import pystray + from PIL import Image +else: + import gi + import psutil + gi.require_version('Gtk', '3.0') + gi.require_version('AppIndicator3', '0.1') + from gi.repository import AppIndicator3, Gtk + from PIL import Image + + +# Detect installation directory +if IS_WINDOWS: + if getattr(sys, 'frozen', False): + _install_dir = os.path.dirname(sys.executable) + else: + _install_dir = os.path.dirname(os.path.abspath(__file__)) + binaries = [ + os.path.join(_install_dir, "aw-server-rust", "aw-server-rust.exe"), + os.path.join(_install_dir, "aw-watcher-afk", "aw-watcher-afk.exe"), + os.path.join(_install_dir, "aw-watcher-window", "aw-watcher-window.exe"), + ] +else: + binaries = [ + '/opt/activitywatch/aw-server-rust/aw-server-rust', + '/opt/activitywatch/awatcher/aw-awatcher', + ] + + +def get_icon(): + """Generate an eye icon in Odoo purple color. Returns PIL Image.""" img = Image.new('RGBA', (64, 64), (0, 0, 0, 0)) for x in range(64): for y in range(64): pos = (x - 32) ** 2 + (y - 32) ** 2 if 300 < pos < 900 or pos < 100: img.putpixel((x, y), (128, 0, 128, 255)) + return img + +def get_icon_path(): + """Save icon to temp file and return path (Linux only).""" + img = get_icon() temp_dir = tempfile.gettempdir() icon_path = os.path.join(temp_dir, "my-aw-icon.png") img.save(icon_path) @@ -36,11 +60,22 @@ def get_icon_path(): def systray_already_running(): - return len([p for p in psutil.process_iter(['cmdline']) if p.info['cmdline'] and p.info['cmdline'][-1].endswith('aw-systray-odoo.py')]) > 1 + if IS_WINDOWS: + return False + else: + return len([p for p in psutil.process_iter(['cmdline']) if p.info['cmdline'] and p.info['cmdline'][-1].endswith('aw-systray-odoo.py')]) > 1 def notify(message): - subprocess.run(['notify-send', "Odoo Activity Watch", message], check=False) + if IS_WINDOWS: + try: + from win10toast import ToastNotifier + ToastNotifier().show_toast("Odoo Activity Watch", message, duration=3) + except ImportError: + import ctypes + ctypes.windll.user32.MessageBoxW(0, message, "Odoo Activity Watch", 0) + else: + subprocess.run(['notify-send', "Odoo Activity Watch", message], check=False) class ActivityWatchMonitor: @@ -50,8 +85,10 @@ def __init__(self, indicator): self.indicator = indicator def check_extension(self): + if IS_WINDOWS: + return result = subprocess.run(['gnome-extensions', 'list', '--enabled'], capture_output=True, text=True, check=False) - if not 'focused-window-dbus@flexagoon.com' in result.stdout.split('\n'): + if 'focused-window-dbus@flexagoon.com' not in result.stdout.split('\n'): subprocess.run(['gnome-extensions', 'enable', 'focused-window-dbus@flexagoon.com'], capture_output=True, text=True, check=False) def stop_server(self, widget=None): @@ -62,7 +99,7 @@ def stop_server(self, widget=None): self.procs = [] def open_ui(self, widget=None): - webbrowser.open("http://127.0.1:5600") + webbrowser.open("http://127.0.0.1:5600") def about(self, widget=None): webbrowser.open("https://www.odoo.com/odoo-19-1-release-notes#:~:text=the%20list%20view.-,Timesheets,-ActivityWatch%20integration") @@ -71,20 +108,31 @@ def start_server(self, widget=None): self.check_extension() self.stop_server() for binary in binaries: - b = subprocess.Popen(binary, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + if not os.path.exists(binary): + notify(f"Binary not found: {binary}") + continue + startupinfo = None + if IS_WINDOWS: + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + b = subprocess.Popen(binary, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, startupinfo=startupinfo) b.poll() if b.returncode is None: self.procs.append(b) else: b.wait() - notify(f"{b.args} Not started") + notify(f"{binary} not started") def on_quit(self, widget=None): self.stop_server() - Gtk.main_quit() + if IS_WINDOWS: + if self.indicator: + self.indicator.stop() + else: + Gtk.main_quit() -def create_menu(indicator, monitor): +def create_menu_linux(indicator, monitor): menu = Gtk.Menu() item_ui = Gtk.MenuItem(label="ActivityWatch UI") @@ -113,21 +161,43 @@ def create_menu(indicator, monitor): return menu +def create_menu_windows(monitor): + return ( + pystray.MenuItem("ActivityWatch UI", monitor.open_ui, default=True), + pystray.MenuItem("Start Server", monitor.start_server), + pystray.MenuItem("Stop Server", monitor.stop_server), + pystray.MenuItem("About", monitor.about), + pystray.Menu.SEPARATOR, + pystray.MenuItem("Exit", monitor.on_quit), + ) + + if __name__ == '__main__': if systray_already_running(): notify("Systray app is already running !") sys.exit(0) - icon_path = get_icon_path() - indicator = AppIndicator3.Indicator.new( - "Odoo ActivityWatch", - icon_path, - AppIndicator3.IndicatorCategory.APPLICATION_STATUS, - ) - - indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE) - - awmon = ActivityWatchMonitor(indicator) - indicator.set_menu(create_menu(indicator, awmon)) - - Gtk.main() + if IS_WINDOWS: + icon = get_icon() + monitor = ActivityWatchMonitor(None) + indicator = pystray.Icon( + "Odoo ActivityWatch", + icon, + "Odoo ActivityWatch", + create_menu_windows(monitor) + ) + monitor.indicator = indicator + indicator.run() + else: + icon_path = get_icon_path() + indicator = AppIndicator3.Indicator.new( + "Odoo ActivityWatch", + icon_path, + AppIndicator3.IndicatorCategory.APPLICATION_STATUS, + ) + indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE) + + awmon = ActivityWatchMonitor(indicator) + indicator.set_menu(create_menu_linux(indicator, awmon)) + + Gtk.main() From e39d013098f7b7e90a8e9d7f445aa80e656438b3 Mon Sep 17 00:00:00 2001 From: Christophe Monniez Date: Fri, 3 Apr 2026 10:55:40 +0200 Subject: [PATCH 4/9] [IMP] packaging: add spec file for aw-systray-odoo Add a .spec file used by pyinstaller to build aw-systray-odoo for windows platform. --- odoo-setup/aw-systray-odoo.spec | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 odoo-setup/aw-systray-odoo.spec diff --git a/odoo-setup/aw-systray-odoo.spec b/odoo-setup/aw-systray-odoo.spec new file mode 100644 index 000000000..42a38368a --- /dev/null +++ b/odoo-setup/aw-systray-odoo.spec @@ -0,0 +1,28 @@ +# -*- mode: python -*- +# vi: set ft=python : + +a = Analysis( + ['aw-systray-odoo.py'], + pathex=[], + binaries=[], + datas=[], + hiddenimports=['pystray', 'pystray._win32', 'PIL'], + hookspath=[], + runtime_hooks=[], + excludes=['gi', 'psutil', 'win10toast'], + win_no_prefer_redirects=False, + win_private_assemblies=False, +) +pyz = PYZ(a.pure, a.zipped_data) +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + name='aw-systray-odoo', + debug=False, + strip=False, + upx=True, + console=False, +) From a61a11c0f30686d026033df010df7e4a04a3fbe4 Mon Sep 17 00:00:00 2001 From: Christophe Monniez Date: Fri, 3 Apr 2026 11:09:40 +0200 Subject: [PATCH 5/9] [IMP] aw-server-rust: update aw-server-rust commit to latest master --- aw-server-rust | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aw-server-rust b/aw-server-rust index 9c125970f..d1f224051 160000 --- a/aw-server-rust +++ b/aw-server-rust @@ -1 +1 @@ -Subproject commit 9c125970fbde3a984ac0ee67f5e6fb9365f57484 +Subproject commit d1f224051f42348b455ec4634bae029d2618defd From 9ed7f68215613e18a65bba67143197c532a701b0 Mon Sep 17 00:00:00 2001 From: Xavier-Do Date: Fri, 10 Apr 2026 14:28:27 +0200 Subject: [PATCH 6/9] [IMP] aw-systray: simplify platform dependant code --- odoo-setup/aw-systray-odoo.py | 122 +++++++++++++--------------------- 1 file changed, 48 insertions(+), 74 deletions(-) diff --git a/odoo-setup/aw-systray-odoo.py b/odoo-setup/aw-systray-odoo.py index 328b68da8..c35b06d8b 100644 --- a/odoo-setup/aw-systray-odoo.py +++ b/odoo-setup/aw-systray-odoo.py @@ -49,16 +49,6 @@ def get_icon(): img.putpixel((x, y), (128, 0, 128, 255)) return img - -def get_icon_path(): - """Save icon to temp file and return path (Linux only).""" - img = get_icon() - temp_dir = tempfile.gettempdir() - icon_path = os.path.join(temp_dir, "my-aw-icon.png") - img.save(icon_path) - return icon_path - - def systray_already_running(): if IS_WINDOWS: return False @@ -132,72 +122,56 @@ def on_quit(self, widget=None): Gtk.main_quit() -def create_menu_linux(indicator, monitor): - menu = Gtk.Menu() - - item_ui = Gtk.MenuItem(label="ActivityWatch UI") - item_ui.connect("activate", monitor.open_ui) - menu.append(item_ui) - - item_start = Gtk.MenuItem(label="Start Server") - item_start.connect("activate", monitor.start_server) - menu.append(item_start) - - item_stop = Gtk.MenuItem(label="Stop Server") - item_stop.connect("activate", monitor.stop_server) - menu.append(item_stop) - - item_about = Gtk.MenuItem(label="About") - item_about.connect("activate", monitor.about) - menu.append(item_about) - - menu.append(Gtk.SeparatorMenuItem()) - - item_exit = Gtk.MenuItem(label="Exit") - item_exit.connect("activate", monitor.on_quit) - menu.append(item_exit) - - menu.show_all() - return menu - - -def create_menu_windows(monitor): - return ( - pystray.MenuItem("ActivityWatch UI", monitor.open_ui, default=True), - pystray.MenuItem("Start Server", monitor.start_server), - pystray.MenuItem("Stop Server", monitor.stop_server), - pystray.MenuItem("About", monitor.about), - pystray.Menu.SEPARATOR, - pystray.MenuItem("Exit", monitor.on_quit), - ) - +if IS_WINDOWS: + def create_indicator(name, icon, menu_items): + pystray_items = [] + for item in menu_items: + if item.get("is_separator"): + pystray_items.append(pystray.Menu.SEPARATOR) + else: + pystray_items.append( + pystray.MenuItem( + item["label"], + item["action"], + default=item.get("default", False) + ) + ) + indicator = pystray.Icon(name, icon,name, pystray_items) + indicator.run() + return indicator +else: + def create_indicator(name, icon, menu_items): + temp_dir = tempfile.gettempdir() + icon_path = os.path.join(temp_dir, "my-aw-icon.png") + icon.save(icon_path) + indicator = AppIndicator3.Indicator.new(name, icon_path, AppIndicator3.IndicatorCategory.APPLICATION_STATUS,) + indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE) + menu = Gtk.Menu() + for item in menu_items: + if item.get("is_separator"): + menu.append(Gtk.SeparatorMenuItem()) + else: + menu_item = Gtk.MenuItem(label=item["label"]) + menu_item.connect("activate", item["action"]) + menu.append(menu_item) + menu.show_all() + indicator.set_menu(menu) + Gtk.main() + return indicator if __name__ == '__main__': if systray_already_running(): notify("Systray app is already running !") sys.exit(0) - - if IS_WINDOWS: - icon = get_icon() - monitor = ActivityWatchMonitor(None) - indicator = pystray.Icon( - "Odoo ActivityWatch", - icon, - "Odoo ActivityWatch", - create_menu_windows(monitor) - ) - monitor.indicator = indicator - indicator.run() - else: - icon_path = get_icon_path() - indicator = AppIndicator3.Indicator.new( - "Odoo ActivityWatch", - icon_path, - AppIndicator3.IndicatorCategory.APPLICATION_STATUS, - ) - indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE) - - awmon = ActivityWatchMonitor(indicator) - indicator.set_menu(create_menu_linux(indicator, awmon)) - - Gtk.main() + monitor = ActivityWatchMonitor(None) + menu_items = [ + {"label": "ActivityWatch UI", "action": monitor.open_ui, "default": True}, + {"label": "Start Server", "action": monitor.start_server}, + {"label": "Stop Server", "action": monitor.stop_server}, + {"label": "About", "action": monitor.about}, + {"is_separator": True}, + {"label": "Exit", "action": monitor.on_quit}, + ] + icon = get_icon() + indicator = create_indicator("Odoo ActivityWatch", icon, menu_items) + monitor.indicator = indicator From b617947dd101c12222dbc4de1b6aca5889647981 Mon Sep 17 00:00:00 2001 From: Christophe Monniez Date: Tue, 14 Apr 2026 09:45:03 +0200 Subject: [PATCH 7/9] [FIX] properly find windows executables during packaging --- aw.spec | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/aw.spec b/aw.spec index 60a633404..5709b6907 100644 --- a/aw.spec +++ b/aw.spec @@ -83,8 +83,16 @@ restx_path = Path(os.path.dirname(flask_restx.__file__)) aws_location = Path("aw-server") aw_server_rust_location = Path("aw-server-rust") -aw_server_rust_bin = aw_server_rust_location / "target/package/aw-server-rust" -aw_sync_bin = aw_server_rust_location / "target/package/aw-sync" + +# Properly find windows executables +_cargo_target = os.environ.get("CARGO_BUILD_TARGET", "") +if _cargo_target: + aw_server_rust_bin = aw_server_rust_location / "target" / _cargo_target / "release" / "aw-server.exe" + aw_sync_bin = aw_server_rust_location / "target" / _cargo_target / "release" / "aw-sync.exe" +else: + aw_server_rust_bin = aw_server_rust_location / "target/package/aw-server-rust" + aw_sync_bin = aw_server_rust_location / "target/package/aw-sync" + aw_qt_location = Path("aw-qt") awa_location = Path("aw-watcher-afk") aww_location = Path("aw-watcher-window") From 689da218eff80ec6c9461ba1109d83d527cac5d1 Mon Sep 17 00:00:00 2001 From: Christophe Monniez Date: Tue, 14 Apr 2026 10:04:05 +0200 Subject: [PATCH 8/9] [IMP] aw-server-rust: update aw-server-rust commit Update the aw-server-rust commit to the corresponding one with the changes needed for the current branch. (git submodules inferno) --- aw-server-rust | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aw-server-rust b/aw-server-rust index d1f224051..924db4433 160000 --- a/aw-server-rust +++ b/aw-server-rust @@ -1 +1 @@ -Subproject commit d1f224051f42348b455ec4634bae029d2618defd +Subproject commit 924db4433bca3e9d13e99a5192f427aeb5151e5d From 8e476a6e2a1abea0d47c1de8f94f5ce399881fcb Mon Sep 17 00:00:00 2001 From: Christophe Monniez Date: Wed, 15 Apr 2026 13:54:36 +0200 Subject: [PATCH 9/9] [IMP] packaging: add wine based windows packaging Replace the initial KVM VM workflow with a Docker container using Wine for cross-platform Windows builds. - Add Dockerfile_wine: Wine + Python 3.14 + Inno Setup 6 + mingw-w64 + Rust - Add buildwine.sh: orchestrates the full build (Xvfb, Wine Python, cross-compilation via MinGW, PyInstaller, Inno Setup) - Makefile: add $(PYTHON) for Wine Python, exclude watchers from submodule loop - getversion.sh: add AW_VERSION environment variable - package-all.sh: add AW_PLATFORM and INNOSETUPDIR for Inno Setup via Wine - includes aw-systray-odoo.exe in NSIS installer The main idea is to cross compile rust binaries for windows within a Docker container. For that, the Cargo build target "x86_64-pc-windows-gnu" is used. On the other hand, the wine is needed for pyinstaller and Inno Setup. --- Makefile | 8 +- odoo-setup/Dockerfile_windows | 74 +++++++++++++++++++ odoo-setup/README.md | 57 +++++++++++++++ odoo-setup/buildpackage.sh | 58 +++++++++------ odoo-setup/buildwine.sh | 130 +++++++++++++++++++++++++++++++++ scripts/package/getversion.sh | 4 +- scripts/package/package-all.sh | 19 ++++- 7 files changed, 318 insertions(+), 32 deletions(-) create mode 100644 odoo-setup/Dockerfile_windows create mode 100644 odoo-setup/README.md create mode 100755 odoo-setup/buildwine.sh diff --git a/Makefile b/Makefile index 9c623d59a..c59ef799b 100644 --- a/Makefile +++ b/Makefile @@ -196,6 +196,9 @@ package: rm -rf dist mkdir -p dist/activitywatch for dir in $(PACKAGEABLES); do \ + if [[ "$(ODOO_WINDOWS_BUILD)" == "true" ]] && [[ "$$dir" == "aw-watcher-afk" || "$$dir" == "aw-watcher-window" ]]; then \ + continue; \ + fi; \ make --directory=$$dir package; \ cp -r $$dir/dist/$$dir dist/activitywatch; \ done @@ -205,11 +208,8 @@ ifeq ($(TAURI_BUILD),true) cp aw-server-rust/target/$(targetdir)/aw-sync dist/activitywatch/aw-server-rust/aw-sync else ifeq ($(ODOO_WINDOWS_BUILD),true) -# ODOO_WINDOWS_BUILD: Install pystray and build aw-systray-odoo.exe via PyInstaller - @echo "ODOO_WINDOWS_BUILD: Installing pystray..." - python -m pip install pystray pillow pywin32 @echo "ODOO_WINDOWS_BUILD: Building aw-systray-odoo.exe via PyInstaller..." - pyinstaller --clean --noconfirm odoo-setup/aw-systray-odoo.spec + $(PYTHON) -m PyInstaller --clean --noconfirm odoo-setup/aw-systray-odoo.spec cp dist/aw-systray-odoo.exe dist/activitywatch/aw-systray-odoo.exe endif endif diff --git a/odoo-setup/Dockerfile_windows b/odoo-setup/Dockerfile_windows new file mode 100644 index 000000000..76f784bcc --- /dev/null +++ b/odoo-setup/Dockerfile_windows @@ -0,0 +1,74 @@ +FROM debian:trixie + +ENV DEBIAN_FRONTEND=noninteractive +ENV LC_ALL=C.UTF-8 + +RUN dpkg --add-architecture i386 + +ARG USID=1000 GRID=1000 +RUN groupadd -g $GRID odoo \ + && useradd --create-home -u $USID -g odoo -G audio,video odoo \ + && mkdir -p /run/user/$USID \ + && chmod 700 /run/user/$USID + +RUN apt-get update \ + && apt-get install -y curl unzip 7zip xvfb mingw-w64 rustup make build-essential git zip \ + python3 python3-pip python3-venv python3-poetry \ + && rm -rf /var/lib/apt/lists/* + +RUN mkdir -pm755 /etc/apt/keyrings \ + && curl -sSL https://dl.winehq.org/wine-builds/winehq.key -o /etc/apt/keyrings/winehq-archive.key \ + && curl -sSL https://dl.winehq.org/wine-builds/debian/dists/trixie/winehq-trixie.sources -o /etc/apt/sources.list.d/winehq.sources \ + && apt-get update \ + && apt-get install --install-recommends -y winehq-stable \ + && rm -rf /var/lib/apt/lists/* + +USER odoo + +ENV NVM_DIR=/home/odoo/.nvm +ENV NODE_VERSION=22.22.1 +RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash && \ + bash -c "source $NVM_DIR/nvm.sh && \ + nvm install $NODE_VERSION && \ + nvm alias default $NODE_VERSION && \ + nvm use default" + +ENV PATH="$NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH" + +RUN rustup toolchain install nightly \ + && rustup default nightly \ + && rustup target add x86_64-pc-windows-gnu + +RUN python3 -m venv /home/odoo/awvenv +ENV PATH="/home/odoo/awvenv/bin:$PATH" +ENV VIRTUAL_ENV=/home/odoo/awvenv + +ENV XDG_RUNTIME_DIR=/run/user/$USID + +ENV WINEPREFIX=/home/odoo/.wine +ENV WINEARCH=win64 +# Ignoring debug and fixme's message when running wine +ENV WINEDEBUG=-all + +ENV CARGO_BUILD_TARGET=x86_64-pc-windows-gnu + +RUN wineboot --init && wineserver -w + +RUN curl -fsSL https://www.python.org/ftp/python/3.14.4/python-3.14.4-amd64.exe -o /tmp/python-installer.exe \ + && chmod u+x /tmp/python-installer.exe + +RUN xvfb-run --auto-servernum --server-args="-screen 0 1024x768x24" \ + sh -c "wine /tmp/python-installer.exe /quiet Include_doc=0 InstallAllUsers=1 PrependPath=1 \ + && (while pgrep -u $(id -u) -x wineserver > /dev/null 2>&1; do sleep 1; done) \ + && wineserver -w" + +RUN PYTHON_EXE="/home/odoo/.wine/drive_c/Program Files/Python314/python.exe" \ + && PIP_EXE="/home/odoo/.wine/drive_c/Program Files/Python314/Scripts/pip.exe" \ + && xvfb-run --auto-servernum --server-args="-screen 0 1024x768x24" \ + sh -c "wine '$PIP_EXE' install pystray pillow pywin32 pyinstaller" + +RUN curl -fsSL https://github.com/jrsoftware/issrc/releases/download/is-6_7_1/innosetup-6.7.1.exe -o /tmp/innosetup.exe \ + && chmod u+x /tmp/innosetup.exe \ + && xvfb-run --auto-servernum wine /tmp/innosetup.exe /VERYSILENT /SUPPRESSMSGBOXES /NORESTART \ + && (while pgrep -u $(id -u) -x wineserver > /dev/null 2>&1; do sleep 1; done) \ + && wineserver -w diff --git a/odoo-setup/README.md b/odoo-setup/README.md new file mode 100644 index 000000000..e2464330b --- /dev/null +++ b/odoo-setup/README.md @@ -0,0 +1,57 @@ +# ActivityWatch Odoo Package Build + +Build script for Odoo version of ActivityWatch packages for Linux (Debian/Ubuntu) and Windows. + +## Requirements + +- Docker +- `realpath` (coreutils) +- Sufficient disk space (~2GB for Docker images + build artifacts) + +## Usage + +```bash +./buildpackage.sh [package_name] +``` + +### Arguments + +| Argument | Description | Default | +|---|---|---| +| `distro` | Target platform (`noble`, `jammy`, `windows`) | `noble` | +| `package_name` | Output package name | `activitywatch-odoo-` | + +### Examples + +```bash +# Build Ubuntu 24.04 package +./buildpackage.sh noble + +# Build Ubuntu 22.04 package +./buildpackage.sh jammy + +# Build Windows installer +./buildpackage.sh windows + +# Custom package name +./buildpackage.sh jammy activitywatch-custom +``` + +## Outputs + +After a successful build, artifacts are available in `odoo-setup/dist/`: + +| Platform | Output | +|---|---| +| Linux | `activitywatch-odoo-.deb` | +| Windows | `activitywatch-odoo-YYYY-MM-DD-windows-x86_64-setup.exe` + `.zip` | + +## Troubleshooting + +### Docker permission errors + +Ensure your user is in the `docker` group: + +```bash +sudo usermod -aG docker $USER +``` diff --git a/odoo-setup/buildpackage.sh b/odoo-setup/buildpackage.sh index 5cd63d270..c98760942 100755 --- a/odoo-setup/buildpackage.sh +++ b/odoo-setup/buildpackage.sh @@ -2,6 +2,7 @@ set -xe cd "$(dirname "$(realpath "$0")")" || exit +mkdir -p dist DISTRO_NAME="${1:-${DISTRO_NAME:-"noble"}}" PACKAGE_NAME="${2:-${PACKAGE_NAME:-"activitywatch-odoo-$DISTRO_NAME"}}" @@ -11,27 +12,38 @@ DOCKER_TAG="${DISTRO_NAME}-awbuilder" docker build -t "${DOCKER_TAG}" . -f "${DOCKER_FILE}" --build-arg=USID=$(id -u) --build-arg=GRID=$(id -g) -DEBUILD_PATH="/data/build/dist/${PACKAGE_NAME}" +if [[ "$DISTRO_NAME" == "windows" ]]; then + docker run --rm \ + -v "$(realpath ../):/data/build/activitywatch" \ + -w /data/build/activitywatch/odoo-setup \ + -t "${DOCKER_TAG}:latest" \ + ./buildwine.sh +else + DEBUILD_PATH="/data/build/activitywatch/dist/${PACKAGE_NAME}" -docker run --rm -v ../:/data/build/activitywatch -v ./dist:/data/build/dist -t "${DOCKER_TAG}:latest" \ - /bin/bash -c \ - "cd /data/build/activitywatch \ - && mkdir -p ${DEBUILD_PATH} \ - && rm -rf ${DEBUILD_PATH}/* \ - && make clean \ - && make build SUBMODULES='aw-core aw-client aw-qt aw-server aw-server-rust aw-watcher-afk aw-watcher-window awatcher' \ - && make package SUBMODULES='aw-core aw-client aw-qt aw-server aw-server-rust aw-watcher-afk aw-watcher-window awatcher' \ - && unzip /data/build/activitywatch/dist/activitywatch-*-linux-x86_64.zip -d ${DEBUILD_PATH}/opt/ \ - && echo 'Preparing Debian Package' \ - && cd ${DEBUILD_PATH} \ - && cp -rav /data/build/activitywatch/odoo-setup/debian DEBIAN \ - && mkdir -p usr/share/gnome-shell/extensions/ \ - && unzip /data/build/activitywatch/odoo-setup/focused-window-dbus-${DISTRO_NAME}.zip -d usr/share/gnome-shell/extensions/ \ - && cp /data/build/activitywatch/odoo-setup/aw-systray-odoo.py opt/activitywatch/aw-systray-odoo.py \ - && mkdir -p etc/xdg/autostart/ \ - && mkdir -p usr/share/applications/ \ - && cp /data/build/activitywatch/odoo-setup/activitywatch-odoo.desktop etc/xdg/autostart/ \ - && cp /data/build/activitywatch/odoo-setup/activitywatch-odoo.desktop usr/share/applications/ \ - && echo 'Building Debian Package' \ - && cd /data/build/dist \ - && dpkg-deb --root-owner-group -Zxz --build ${PACKAGE_NAME}" + docker run --rm \ + -v "$(realpath ../):/data/build/activitywatch" \ + -w "/data/build/activitywatch" \ + -t "${DOCKER_TAG}:latest" \ + /bin/bash -c " \ + mkdir -p ${DEBUILD_PATH} && \ + rm -rf ${DEBUILD_PATH}/* && \ + make clean && \ + make build SUBMODULES='aw-core aw-client aw-qt aw-server aw-server-rust aw-watcher-afk aw-watcher-window awatcher' && \ + make package SUBMODULES='aw-core aw-client aw-qt aw-server aw-server-rust aw-watcher-afk aw-watcher-window awatcher' && \ + mkdir -p ${DEBUILD_PATH}/opt && \ + unzip /data/build/activitywatch/dist/activitywatch-*-linux-x86_64.zip -d ${DEBUILD_PATH}/opt/ && \ + echo 'Preparing Debian Package' && \ + cd ${DEBUILD_PATH} && \ + cp -rav /data/build/activitywatch/odoo-setup/debian DEBIAN && \ + mkdir -p usr/share/gnome-shell/extensions/ && \ + unzip /data/build/activitywatch/odoo-setup/focused-window-dbus-${DISTRO_NAME}.zip -d usr/share/gnome-shell/extensions/ && \ + cp /data/build/activitywatch/odoo-setup/aw-systray-odoo.py opt/activitywatch/aw-systray-odoo.py && \ + mkdir -p etc/xdg/autostart/ && \ + mkdir -p usr/share/applications/ && \ + cp /data/build/activitywatch/odoo-setup/activitywatch-odoo.desktop etc/xdg/autostart/ && \ + cp /data/build/activitywatch/odoo-setup/activitywatch-odoo.desktop usr/share/applications/ && \ + echo 'Building Debian Package' && \ + cd /data/build/activitywatch/dist && \ + dpkg-deb --root-owner-group -Zxz --build ${PACKAGE_NAME}" +fi diff --git a/odoo-setup/buildwine.sh b/odoo-setup/buildwine.sh new file mode 100755 index 000000000..6e06153de --- /dev/null +++ b/odoo-setup/buildwine.sh @@ -0,0 +1,130 @@ +#!/bin/bash +set -euo pipefail + +: "${CARGO_BUILD_TARGET:=x86_64-pc-windows-gnu}" +: "${ODOO_WINDOWS_BUILD:=true}" +: "${SKIP_SERVER_PYTHON:=true}" +: "${WINEPREFIX:=/home/odoo/.wine}" +: "${WINEARCH:=win64}" + +PYTHON_DIR="$WINEPREFIX/drive_c/Program Files/Python314" +PYTHON_WIN="C:\\Program Files\\Python314\\python.exe" +PIP_WIN="C:\\Program Files\\Python314\\Scripts\\pip.exe" +AW_SOURCE="/data/build/activitywatch" + +export CARGO_BUILD_TARGET ODOO_WINDOWS_BUILD SKIP_SERVER_PYTHON +export WINEPREFIX WINEARCH WINEDEBUG=-all +export XDG_RUNTIME_DIR=/run/user/1000 +export DISPLAY=${DISPLAY:-:99} + +# Python venv wrappers needed for make subshells +export VIRTUAL_ENV=/home/odoo/awvenv +export PATH="/home/odoo/awvenv/bin:$PATH" +mkdir -p /tmp/awvenv-wrappers +cat > /tmp/awvenv-wrappers/pip << 'EOF' +#!/bin/bash +exec /home/odoo/awvenv/bin/pip "$@" +EOF +cat > /tmp/awvenv-wrappers/python << 'EOF' +#!/bin/bash +exec /home/odoo/awvenv/bin/python "$@" +EOF +chmod +x /tmp/awvenv-wrappers/{pip,python} +export PATH="/tmp/awvenv-wrappers:$PATH" +export PYTHON="wine '$PYTHON_WIN'" +export WINE_PYTHON="wine '$PYTHON_WIN'" + +# Xvfb start and stop functions +XVFB_PID="" +_start_xvfb() { + if ! pgrep -x Xvfb > /dev/null 2>&1; then + Xvfb $DISPLAY -screen 0 1024x768x24 & + XVFB_PID=$! + sleep 2 + echo "[Xvfb] started (PID $XVFB_PID)" + fi +} + +_stop_xvfb() { + if [[ -n "$XVFB_PID" ]] && kill -0 "$XVFB_PID" 2>/dev/null; then + kill "$XVFB_PID" + wait "$XVFB_PID" 2>/dev/null || true + echo "[Xvfb] stopped" + fi +} + +trap _stop_xvfb EXIT +_start_xvfb + +# Wine Python helpers +wine_pip() { + xvfb-run --auto-servernum wine "$PIP_WIN" "$@" +} + +wine_python() { + xvfb-run --auto-servernum wine "$PYTHON_WIN" "$@" +} + +echo "[Wine] initializing prefix..." +wineboot --init 2>/dev/null || true +wineserver -w + +echo "[Build] Starting ActivityWatch build..." +export AW_VERSION="0.13.2" +cd "$AW_SOURCE" +make build + +echo "[PyInstaller] Installing dependencies ..." +wine_pip install --no-warn-script-location pystray pillow pynput wmi + +echo "[PyInstaller] Installing aw packages in Wine..." +wine_pip install --no-warn-script-location \ + "$AW_SOURCE/aw-core" \ + "$AW_SOURCE/aw-client" + +echo "[PyInstaller] Building aw-systray-odoo.exe..." +wine_python -m PyInstaller --clean --noconfirm odoo-setup/aw-systray-odoo.spec + +echo "[PyInstaller] Building aw-watcher-afk.exe..." +cd "$AW_SOURCE/aw-watcher-afk" +wine_python -m PyInstaller --clean --noconfirm aw-watcher-afk.spec + +echo "[PyInstaller] Building aw-watcher-window.exe..." +cd "$AW_SOURCE/aw-watcher-window" +wine_python -m PyInstaller --clean --noconfirm aw-watcher-window.spec + +cd "$AW_SOURCE" +STAGING_DIR="$AW_SOURCE/staging-watcher-build" +mkdir -p "$STAGING_DIR" +cp "$AW_SOURCE/dist/aw-systray-odoo.exe" "$STAGING_DIR/" +cp -r "$AW_SOURCE/aw-watcher-afk/dist/aw-watcher-afk" "$STAGING_DIR/" +cp -r "$AW_SOURCE/aw-watcher-window/dist/aw-watcher-window" "$STAGING_DIR/" + +export AW_VERSION="odoo-$(date +%Y-%m-%d)" +export AW_PLATFORM=windows +export INNOSETUPDIR="C:\Program Files (x86)\Inno Setup 6" +echo "[Package] Creating package..." +rm -rf "$AW_SOURCE/dist" +mkdir -p "$AW_SOURCE/dist/activitywatch" +for dir in aw-server-rust; do + make --directory="$AW_SOURCE/$dir" package + cp -r "$AW_SOURCE/$dir/dist/$dir" "$AW_SOURCE/dist/activitywatch/" +done +cp -r "$STAGING_DIR/aw-watcher-afk" "$AW_SOURCE/dist/activitywatch/" +cp -r "$STAGING_DIR/aw-watcher-window" "$AW_SOURCE/dist/activitywatch/" +cp "$STAGING_DIR/aw-systray-odoo.exe" "$AW_SOURCE/dist/activitywatch/" +rm -rf "$STAGING_DIR" + +# Remove problem-causing binaries (see original Makefile) +rm -f "$AW_SOURCE/dist/activitywatch/libdrm.so.2" +rm -f "$AW_SOURCE/dist/activitywatch/libharfbuzz.so.0" +rm -f "$AW_SOURCE/dist/activitywatch/libfontconfig.so.1" +rm -f "$AW_SOURCE/dist/activitywatch/libfreetype.so.6" +rm -rf "$AW_SOURCE/dist/activitywatch/pytz" + +# Build zip and installer +bash "$AW_SOURCE/scripts/package/package-all.sh" + +echo "" +echo "=== Build finished 🎉 ===" +find "$AW_SOURCE/dist" -name "*.exe" -o -name "*.zip" 2>/dev/null | head -20 diff --git a/scripts/package/getversion.sh b/scripts/package/getversion.sh index 9c237975e..8d7900713 100755 --- a/scripts/package/getversion.sh +++ b/scripts/package/getversion.sh @@ -3,7 +3,9 @@ # TODO: Merge with scripts/package/getversion.sh # set -e -if [[ $TRAVIS_TAG ]]; then +if [[ -n "${AW_VERSION:-}" ]]; then + _version="$AW_VERSION"; +elif [[ $TRAVIS_TAG ]]; then _version=$TRAVIS_TAG; elif [[ $APPVEYOR_REPO_TAG_NAME ]]; then _version=$APPVEYOR_REPO_TAG_NAME; diff --git a/scripts/package/package-all.sh b/scripts/package/package-all.sh index ae286c94a..0fbabf3da 100755 --- a/scripts/package/package-all.sh +++ b/scripts/package/package-all.sh @@ -11,6 +11,11 @@ function get_platform() { # Will return "macos" for macOS/OS X # Will return "windows" for Windows/MinGW/msys + if [[ -n "${AW_PLATFORM:-}" ]]; then + echo "$AW_PLATFORM" + return + fi + _platform=$(uname | tr '[:upper:]' '[:lower:]') if [[ $_platform == "darwin" ]]; then _platform="macos"; @@ -67,8 +72,14 @@ function build_setup() { filename="activitywatch-${version}-${platform}-${arch}-setup.exe" echo "Name of package will be: $filename" - innosetupdir="/c/Program Files (x86)/Inno Setup 6" - if [ ! -d "$innosetupdir" ]; then + : "${INNOSETUPDIR:=}" + if [[ -n "$INNOSETUPDIR" ]] && [ -d "$INNOSETUPDIR" ]; then + innosetupdir="$INNOSETUPDIR" + elif [ -d "/c/Program Files (x86)/Inno Setup 6" ]; then + innosetupdir="/c/Program Files (x86)/Inno Setup 6" + elif command -v winepath > /dev/null && winepath -u "C:\\Program Files (x86)\\Inno Setup 6" &>/dev/null; then + innosetupdir="$(winepath -u 'C:\Program Files (x86)\Inno Setup 6')" + else echo "ERROR: Couldn't find innosetup which is needed to build the installer. We suggest you install it using chocolatey. Exiting." exit 1 fi @@ -76,9 +87,9 @@ function build_setup() { # Windows installer version should not include 'v' prefix, see: https://github.com/microsoft/winget-pkgs/pull/17564 version_no_prefix="$(echo $version | sed -e 's/^v//')" if [[ $TAURI_BUILD == "true" ]]; then - env AW_VERSION=$version_no_prefix "$innosetupdir/iscc.exe" scripts/package/aw-tauri.iss + env AW_VERSION=$version_no_prefix wine "$innosetupdir/iscc.exe" scripts/package/aw-tauri.iss else - env AW_VERSION=$version_no_prefix "$innosetupdir/iscc.exe" scripts/package/activitywatch-setup.iss + env AW_VERSION=$version_no_prefix wine "$innosetupdir/iscc.exe" scripts/package/activitywatch-setup.iss fi mv dist/activitywatch-setup.exe dist/$filename echo "Setup built!"