diff --git a/Makefile b/Makefile index 9b2a5c23c..c59ef799b 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 @@ -186,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 @@ -194,10 +207,11 @@ 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) + @echo "ODOO_WINDOWS_BUILD: Building aw-systray-odoo.exe via PyInstaller..." + $(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 # Remove problem-causing binaries rm -f dist/activitywatch/libdrm.so.2 # see: https://github.com/ActivityWatch/activitywatch/issues/161 diff --git a/aw-server-rust b/aw-server-rust index 9c125970f..924db4433 160000 --- a/aw-server-rust +++ b/aw-server-rust @@ -1 +1 @@ -Subproject commit 9c125970fbde3a984ac0ee67f5e6fb9365f57484 +Subproject commit 924db4433bca3e9d13e99a5192f427aeb5151e5d diff --git a/aw.spec b/aw.spec index 70e2e5b19..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") @@ -101,16 +109,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 +199,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 +214,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 +224,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 +239,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) 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/aw-systray-odoo.py b/odoo-setup/aw-systray-odoo.py index cea629011..c35b06d8b 100644 --- a/odoo-setup/aw-systray-odoo.py +++ b/odoo-setup/aw-systray-odoo.py @@ -1,46 +1,71 @@ -#!/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)) - - temp_dir = tempfile.gettempdir() - icon_path = os.path.join(temp_dir, "my-aw-icon.png") - img.save(icon_path) - return icon_path - + return img 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 +75,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 +89,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,63 +98,80 @@ 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() - - -def create_menu(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 - + if IS_WINDOWS: + if self.indicator: + self.indicator.stop() + else: + Gtk.main_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) - - 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() + 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 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, +) 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/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 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!"