Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@ jobs:
windows:
name: Windows release package
runs-on: windows-latest
timeout-minutes: 30
timeout-minutes: 60
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
with:
# Full history so build.py can read the latest tag for the exe version.
fetch-depth: 0

- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
Expand All @@ -29,24 +32,31 @@ jobs:

- name: Build Windows binaries
working-directory: windows
run: python build.py
run: python build.py --installer

- name: Package Windows release
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path release\DualTouch-windows | Out-Null
Copy-Item -LiteralPath windows\dist\DualTouch-windows.exe -Destination release\DualTouch-windows\
New-Item -ItemType Directory -Force -Path release | Out-Null
# Nuitka onedir build: ship the whole dist folder (exe + helper + data) as-is.
Copy-Item -LiteralPath windows\dist\DualTouch-windows -Destination release\DualTouch-windows -Recurse
Copy-Item -LiteralPath README.md,LICENSE -Destination release\DualTouch-windows\
Compress-Archive -Path release\DualTouch-windows -DestinationPath DualTouch-windows.zip -Force
Compress-Archive -Path release\DualTouch-windows -DestinationPath DualTouch-windows-portable.zip -Force
# Inno Setup installer as its own release asset (built into windows\dist).
Copy-Item -Path windows\dist\DualTouch-windows-setup-*.exe -Destination .

- name: Upload Windows artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
with:
name: DualTouch-windows
path: DualTouch-windows.zip
path: |
DualTouch-windows-portable.zip
DualTouch-windows-setup-*.exe

- name: Attach Windows artifact to tagged release
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 #v3.0.2
with:
files: DualTouch-windows.zip
files: |
DualTouch-windows-portable.zip
DualTouch-windows-setup-*.exe
24 changes: 21 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,12 @@ It is a Windows-only fork of [SteamlessKeyboard](https://github.com/PietPetGit/S

**Release build**

Run `DualTouch-windows.exe` (it will request elevation on start).
Run `DualTouch-windows.exe` from the extracted `DualTouch-windows`
folder (it will request elevation on start) — the whole folder ships
together, so keep all of its files side by side. The
`DualTouch-cursor-helper.exe` next to it is the same binary under its
cursor-helper name (so the two processes are distinguishable in Task
Manager); keep it too.

- Settings: `%APPDATA%\DualTouch\settings.json` (auto-created with defaults; a legacy file next to the exe is migrated automatically)
- Logs: `%APPDATA%\DualTouch\dualtouch.log`
Expand Down Expand Up @@ -95,10 +100,23 @@ menus), or by hand-editing `settings.json` for finer control.

```sh
cd windows
python build.py
python build.py # folder distribution
python build.py --installer # + Windows installer (needs Inno Setup 6)
```

Output: `windows/dist/DualTouch-windows.exe`.
Output: `windows/dist/DualTouch-windows/` (a standalone folder
distribution — `DualTouch-windows.exe`, the `DualTouch-cursor-helper.exe`
copy, and everything they need; ship the whole folder together).

With `--installer`, an Inno Setup 6 setup is compiled to
`windows/dist/DualTouch-windows-setup-<version>.exe`
([Inno Setup](https://jrsoftware.org/isdl.php) must be installed or
`ISCC.exe` on PATH).

Releases are compiled with [Nuitka](https://nuitka.net): all Python is
turned into machine code, so the bundle contains no Python sources or
bytecode. The first build on a machine without a C compiler auto-downloads
MinGW and takes several minutes; later builds are faster.

## Troubleshooting

Expand Down
4 changes: 3 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,6 @@ vdf
PyGObject<3.52 ; sys_platform == "linux" # gi / AppIndicator tray backend (linux/tray_linux.py)

# ---- build tool (used by CI to produce the release binaries) ----
pyinstaller
# Nuitka compiles the whole app to machine code: release bundles ship no
# Python sources or bytecode (see windows/build.py).
nuitka>=2.4
39 changes: 0 additions & 39 deletions windows/DualTouch-windows.spec

This file was deleted.

30 changes: 24 additions & 6 deletions windows/applog.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,41 @@


def _is_frozen():
return getattr(sys, "frozen", False)
"""True when running from a compiled distribution. PyInstaller sets
sys.frozen; Nuitka does not — its marker is the module-level
__compiled__ attribute present in every module it compiles."""
return getattr(sys, "frozen", False) or "__compiled__" in globals()


def _exe_path():
"""Path of the binary the user actually launched.

Nuitka standalone reports sys.executable as '<dist>\\python.exe' even
though no such file exists in the output folder, so anything that
persists, relaunches or advertises a path must ask Windows for our own
process image instead (GetModuleFileNameW(NULL))."""
if _is_frozen():
import ctypes

buf = ctypes.create_unicode_buffer(1024)
if ctypes.windll.kernel32.GetModuleFileNameW(None, buf, 1024):
return os.path.normpath(buf.value)
return os.path.abspath(sys.executable)


def _bundle_dir():
"""Directory containing read-only bundled resources (data/, glyphs)."""
if _is_frozen():
# PyInstaller sets sys._MEIPASS at bootstrap; not in typeshed stubs.
return getattr(
sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__))
)
# Compiled (Nuitka standalone) builds keep every resource next to the
# executable, so the exe dir IS the bundle dir.
return os.path.dirname(_exe_path())
return os.path.dirname(os.path.abspath(__file__))


def _exe_dir():
"""Directory we treat as the install location (for portable settings)."""
if _is_frozen():
return os.path.dirname(os.path.abspath(sys.executable))
return os.path.dirname(_exe_path())
return os.path.dirname(os.path.abspath(__file__))


Expand Down
10 changes: 8 additions & 2 deletions windows/autostart.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@


def _is_frozen():
return getattr(sys, "frozen", False)
# applog-style detection: Nuitka doesn't set sys.frozen (PyInstaller
# does); its marker is the compiled module's __compiled__ attribute.
return getattr(sys, "frozen", False) or "__compiled__" in globals()


def _startup_dir():
Expand Down Expand Up @@ -67,7 +69,11 @@ def _command():
"""(executable, arguments) the scheduled task should run. Frozen: the EXE
itself. From source: the current interpreter running the tray package."""
if _is_frozen():
return os.path.abspath(sys.executable), ""
# applog._exe_path, not sys.executable: Nuitka standalone reports a
# python.exe that doesn't exist in the dist folder.
from applog import _exe_path

return os.path.abspath(_exe_path()), ""
return os.path.abspath(sys.executable), "-m tray"


Expand Down
Loading