Publish a Windows setup beside the AppImage and the disk image

The releases page had nothing for Windows, so the only way in was a
checkout, a Python and a pip install. What goes out now is one setup
program per release: PyInstaller's directory, the pinned ffmpeg the
disk image already uses, and Inno Setup around both. It installs for
the account alone, so no administrator is asked for.

Two executables over the one program there, because a windowed one on
Windows has no standard output at all: Dikte.exe for the Start Menu and
dikte.exe for the terminal, sharing everything they carry. The icon is
drawn by Dikte itself into an .ico, the way the Mac's .icns and Linux's
PNGs already are, so there is still no image file in the repository.

Starting at sign-in is a registry value rather than a Startup shortcut,
which is what lets the setup program, the uninstaller and
`dikte integrate` all mean the same thing: the wizard asks once, and
typing the command changes the answer later.

The three builds move into build.yml, which release.yml now calls
instead of holding its own copy, and which a pull request touching the
packaging runs on its own. A broken build is then a red pull request
rather than a failed release.
This commit is contained in:
2026-08-18 16:08:47 +03:00
parent d24da7df52
commit 23d56acfb5
17 changed files with 696 additions and 132 deletions
+69
View File
@@ -451,5 +451,74 @@ class MacOS(Home):
self.assertIn("install-mac.sh", command.read_text())
class Windows(unittest.TestCase):
"""The half of the Windows install the setup program cannot do.
It writes the Start Menu entry, the command and the uninstaller as it runs,
and asks once whether Dikte should start at sign-in. Changing that answer
afterwards is what is left here, and the value is faked rather than the
registry, so that all of it is read on every platform the tests run on.
"""
def setUp(self):
self.value = ""
for name, function in (("_run_entry", lambda: self.value),
("_write_run_entry", self._write),
("_delete_run_entry", self._delete)):
patch = mock.patch.object(integrate, name, function)
patch.start()
self.addCleanup(patch.stop)
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.installed = pathlib.Path(self.tmp.name).resolve()
self.app = self.installed / "Dikte.exe"
self.app.write_text("")
def _write(self, command):
self.value = command
def _delete(self):
there, self.value = bool(self.value), ""
return there
def install(self, force=False):
with Frozen(str(self.app), platform="win32"):
return integrate.install(force=force)
def remove(self):
with Frozen(str(self.app), platform="win32"):
return integrate.remove()
def test_the_windowed_executable_is_what_starts_at_sign_in(self):
"""The console one is what the `dikte` command runs, and a sign-in that
started that would open a console window nobody asked for."""
with Frozen(str(self.installed / "dikte.exe"), platform="win32"):
self.assertEqual(integrate.target(), self.app)
def test_a_start_does_not_turn_it_on_for_somebody_who_said_no(self):
self.assertEqual(self.install(), [])
self.assertEqual(self.value, "")
def test_typing_it_turns_starting_at_sign_in_on(self):
self.assertEqual(len(self.install(force=True)), 1)
self.assertEqual(self.value, f'"{self.app}"')
def test_an_installation_that_moved_is_pointed_at_where_it_is_now(self):
self.value = '"D:\\Dikte\\Dikte.exe"'
self.assertEqual(len(self.install()), 1)
self.assertEqual(self.value, f'"{self.app}"')
def test_running_it_again_changes_nothing(self):
self.install(force=True)
self.assertEqual(self.install(), [])
def test_removing_stops_it_starting_and_says_so_once(self):
self.install(force=True)
self.assertEqual(len(self.remove()), 1)
self.assertEqual(self.value, "")
self.assertEqual(self.remove(), [])
if __name__ == "__main__":
unittest.main()
+22
View File
@@ -7,6 +7,8 @@ test blends each icon onto a bar of its own and asks whether anything of it
survives, once over black and once over white.
"""
import pathlib
import struct
import sys
import tempfile
import unittest
@@ -150,6 +152,26 @@ class ApplicationIcon(DikteTest):
self.assertFalse(icon.isNull())
self.assertIn(48, [size.width() for size in icon.availableSizes()])
def test_the_windows_icon_is_one_file_holding_every_size(self):
"""Written by hand, so the header is what a test can be wrong about:
Windows reads the sizes out of the directory at the front rather than
out of the images, and a 256 is written there as a zero."""
with tempfile.TemporaryDirectory() as root:
path = trayicon.write_ico(pathlib.Path(root) / "Dikte.ico")
data = path.read_bytes()
reserved, kind, count = struct.unpack_from("<HHH", data)
self.assertEqual((reserved, kind), (0, 1))
self.assertEqual(count, len(trayicon.ICO_SIZES))
for index, size in enumerate(trayicon.ICO_SIZES):
with self.subTest(size=size):
(width, height, _, _, _, _,
length, offset) = struct.unpack_from("<BBBBHHII", data,
6 + 16 * index)
self.assertEqual((width, height), (size % 256, size % 256))
image = QImage.fromData(data[offset:offset + length])
self.assertEqual((image.width(), image.height()),
(size, size))
if __name__ == "__main__":
unittest.main()