mirror of
https://github.com/yusufipk/dikte.git
synced 2026-09-11 10:56:10 +00:00
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:
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env pwsh
|
||||
# The Windows download: one setup program, carrying everything Dikte needs to
|
||||
# record and to be started again after a sign-in.
|
||||
#
|
||||
# Run from anywhere; it works in build\ at the top of the checkout and leaves
|
||||
# the finished .exe in dist\. x64 only, because that is what the PyQt6 wheel and
|
||||
# whisper.cpp both publish for Windows; a Windows on ARM machine runs it under
|
||||
# the emulation it runs everything else under.
|
||||
#
|
||||
# powershell -ExecutionPolicy Bypass -File packaging\build-windows.ps1
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$build = Join-Path $root "build"
|
||||
$out = Join-Path $root "dist"
|
||||
$dist = Join-Path $build "dist\dikte"
|
||||
|
||||
$env:PYTHONPATH = $root
|
||||
$version = & python -c "import dikte; print(dikte.__version__)"
|
||||
if ($LASTEXITCODE -ne 0) { throw "could not read the version out of dikte/__init__.py" }
|
||||
|
||||
# A pinned tag and a checksum rather than "whatever is newest": this binary goes
|
||||
# out inside something people run, so what it is has to be decided here and not
|
||||
# by whoever pushes to that repository next. The same release the disk image
|
||||
# takes its ffmpeg from, which is gyan.dev's essentials build repackaged, and
|
||||
# dshow is in it, which is the one part of ffmpeg recording here goes through.
|
||||
$ffmpegTag = "b6.1.1"
|
||||
$ffmpegAsset = "ffmpeg-win32-x64.gz"
|
||||
$ffmpegSha = "8883A3DFFBD0A16CF4EF95206EA05283F78908DBFB118F73C83F4951DCC06D77"
|
||||
|
||||
if (Test-Path $build) { Remove-Item $build -Recurse -Force }
|
||||
if (Test-Path $out) { Remove-Item $out -Recurse -Force }
|
||||
New-Item -ItemType Directory -Path $build, $out | Out-Null
|
||||
|
||||
# 1. The icon ---------------------------------------------------------------
|
||||
# Drawn by Dikte itself, offscreen, which is why there is no image file in the
|
||||
# repository. Before the application, because PyInstaller writes it into the
|
||||
# executable rather than beside it, and the setup program uses the same file.
|
||||
$icon = Join-Path $build "Dikte.ico"
|
||||
$env:QT_QPA_PLATFORM = "offscreen"
|
||||
& python -m dikte.trayicon --ico $icon
|
||||
if ($LASTEXITCODE -ne 0) { throw "the icon would not draw" }
|
||||
Remove-Item Env:\QT_QPA_PLATFORM
|
||||
$env:DIKTE_ICO = $icon
|
||||
|
||||
# 2. The application --------------------------------------------------------
|
||||
# Two executables in the one directory: Dikte.exe, which is windowed and is
|
||||
# what a shortcut starts, and dikte.exe, which has a console and is what the
|
||||
# `dikte` command runs.
|
||||
& python -m PyInstaller (Join-Path $root "packaging\dikte.spec") `
|
||||
--distpath (Join-Path $build "dist") --workpath (Join-Path $build "work") `
|
||||
--noconfirm --clean
|
||||
if ($LASTEXITCODE -ne 0) { throw "PyInstaller failed" }
|
||||
|
||||
# 3. ffmpeg -----------------------------------------------------------------
|
||||
# Recording on Windows goes through ffmpeg's DirectShow input, and Windows
|
||||
# ships nothing like it, so without this the download would be an application
|
||||
# that cannot record until the person who downloaded it installs one. bin\
|
||||
# beside the executables, because integrate.py puts that directory in front of
|
||||
# PATH at startup and everything reaching for ffmpeg goes through shutil.which.
|
||||
$archive = Join-Path $build $ffmpegAsset
|
||||
Invoke-WebRequest -UseBasicParsing -OutFile $archive `
|
||||
"https://github.com/eugeneware/ffmpeg-static/releases/download/$ffmpegTag/$ffmpegAsset"
|
||||
$got = (Get-FileHash $archive -Algorithm SHA256).Hash
|
||||
if ($got -ne $ffmpegSha) { throw "ffmpeg checksum: expected $ffmpegSha, got $got" }
|
||||
|
||||
$bin = Join-Path $dist "bin"
|
||||
New-Item -ItemType Directory -Path $bin | Out-Null
|
||||
$compressed = [System.IO.File]::OpenRead($archive)
|
||||
$stream = New-Object System.IO.Compression.GzipStream(
|
||||
$compressed, [System.IO.Compression.CompressionMode]::Decompress)
|
||||
$binary = [System.IO.File]::Create((Join-Path $bin "ffmpeg.exe"))
|
||||
try { $stream.CopyTo($binary) } finally { $binary.Dispose(); $stream.Dispose(); $compressed.Dispose() }
|
||||
|
||||
# 4. The setup program ------------------------------------------------------
|
||||
# Inno Setup comes with the GitHub runner. On a machine that has not got it:
|
||||
# winget install JRSoftware.InnoSetup
|
||||
$iscc = (Get-Command iscc -ErrorAction SilentlyContinue).Source
|
||||
if (-not $iscc) {
|
||||
$iscc = Join-Path ${env:ProgramFiles(x86)} "Inno Setup 6\ISCC.exe"
|
||||
}
|
||||
if (-not (Test-Path $iscc)) {
|
||||
throw "no Inno Setup found. Install it with: winget install JRSoftware.InnoSetup"
|
||||
}
|
||||
& $iscc "/DVersion=$version" "/DSource=$dist" "/DIcon=$icon" `
|
||||
(Join-Path $root "packaging\dikte.iss")
|
||||
if ($LASTEXITCODE -ne 0) { throw "Inno Setup failed" }
|
||||
|
||||
Write-Host "dist\Dikte-$version-x64-setup.exe"
|
||||
@@ -0,0 +1,117 @@
|
||||
; What the Windows download is: the directory PyInstaller built, wrapped in the
|
||||
; setup program Windows expects. Run it through build-windows.ps1, which draws
|
||||
; the icon, builds that directory, puts an ffmpeg in it and passes the version
|
||||
; in; ISCC on its own has none of that.
|
||||
;
|
||||
; Per user rather than per machine. It keeps the whole thing out of the way of
|
||||
; the administrator prompt, which for something a person is trying out is the
|
||||
; difference between a download and a phone call to whoever owns the laptop,
|
||||
; and nothing here writes outside the account anyway.
|
||||
|
||||
#ifndef Version
|
||||
#define Version "0.0.0"
|
||||
#endif
|
||||
#ifndef Source
|
||||
#define Source "..\build\dist\dikte"
|
||||
#endif
|
||||
#ifndef Icon
|
||||
#define Icon "..\build\Dikte.ico"
|
||||
#endif
|
||||
|
||||
[Setup]
|
||||
; The identifier Add/Remove Programs files this under, and what an update
|
||||
; recognises the older installation by. The same one the Mac's login item and
|
||||
; the bundle use, and like those it never changes.
|
||||
AppId=io.github.yusufipk.dikte
|
||||
AppName=Dikte
|
||||
AppVersion={#Version}
|
||||
AppPublisher=Yusuf Ipek
|
||||
AppSupportURL=https://github.com/yusufipk/dikte
|
||||
DefaultDirName={localappdata}\Programs\Dikte
|
||||
DefaultGroupName=Dikte
|
||||
DisableProgramGroupPage=yes
|
||||
PrivilegesRequired=lowest
|
||||
ArchitecturesAllowed=x64compatible
|
||||
ArchitecturesInstallIn64BitMode=x64compatible
|
||||
OutputDir=..\dist
|
||||
OutputBaseFilename=Dikte-{#Version}-x64-setup
|
||||
SetupIconFile={#Icon}
|
||||
UninstallDisplayIcon={app}\Dikte.exe
|
||||
WizardStyle=modern
|
||||
; Most of the download is Qt and ffmpeg, both of which compress well, and the
|
||||
; slower setting is a minute of a build machine's time against a smaller file
|
||||
; for everybody who downloads it.
|
||||
Compression=lzma2/max
|
||||
SolidCompression=yes
|
||||
; An update over a running Dikte would otherwise fail on the executable it
|
||||
; cannot replace. Restart Manager closes it and starts it again afterwards.
|
||||
CloseApplications=yes
|
||||
RestartApplications=yes
|
||||
|
||||
[Languages]
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
|
||||
[Tasks]
|
||||
; On by default: Dikte is a tray application holding a global shortcut, and one
|
||||
; that is not running when you press the key is one that does nothing.
|
||||
Name: "autostart"; Description: "Start Dikte when I sign in"
|
||||
|
||||
[Files]
|
||||
Source: "{#Source}\*"; DestDir: "{app}"; Flags: recursesubdirs ignoreversion
|
||||
|
||||
[Icons]
|
||||
Name: "{autoprograms}\Dikte"; Filename: "{app}\Dikte.exe"
|
||||
|
||||
[Registry]
|
||||
; Starting at sign-in, as a registry value rather than a shortcut in the
|
||||
; Startup folder: it is the one place the setup program, the uninstaller and
|
||||
; `dikte integrate` can all read and write without a COM library between them.
|
||||
Root: HKCU; Subkey: "Software\Microsoft\Windows\CurrentVersion\Run"; \
|
||||
ValueType: string; ValueName: "Dikte"; ValueData: """{app}\Dikte.exe"""; \
|
||||
Flags: uninsdeletevalue; Tasks: autostart
|
||||
; And taking it away again, for an update where the box was unticked. Both
|
||||
; lines delete on uninstall, so an entry `dikte integrate` wrote later goes
|
||||
; too, whichever way it got there.
|
||||
Root: HKCU; Subkey: "Software\Microsoft\Windows\CurrentVersion\Run"; \
|
||||
ValueType: none; ValueName: "Dikte"; \
|
||||
Flags: deletevalue uninsdeletevalue; Tasks: not autostart
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\Dikte.exe"; Description: "Start Dikte"; \
|
||||
Flags: nowait postinstall skipifsilent
|
||||
|
||||
[Code]
|
||||
{ The `dikte` command. WindowsApps is already on the user's PATH, so a .cmd
|
||||
left there runs from any terminal without touching the PATH and without an
|
||||
administrator; the alternative is an environment variable edit that every
|
||||
open terminal misses. It names the console executable, which is the one that
|
||||
can print to the terminal it was typed in. }
|
||||
|
||||
function ShimDir(): String;
|
||||
begin
|
||||
Result := ExpandConstant('{localappdata}\Microsoft\WindowsApps');
|
||||
end;
|
||||
|
||||
function ShimPath(): String;
|
||||
begin
|
||||
Result := ShimDir() + '\dikte.cmd';
|
||||
end;
|
||||
|
||||
procedure CurStepChanged(CurStep: TSetupStep);
|
||||
var
|
||||
Shim: String;
|
||||
begin
|
||||
if CurStep = ssPostInstall then begin
|
||||
if DirExists(ShimDir()) then begin
|
||||
Shim := '@echo off' + #13#10
|
||||
+ '"' + ExpandConstant('{app}\dikte.exe') + '" %*' + #13#10;
|
||||
SaveStringToFile(ShimPath(), Shim, False);
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
|
||||
begin
|
||||
if CurUninstallStep = usUninstall then
|
||||
DeleteFile(ShimPath());
|
||||
end;
|
||||
+35
-10
@@ -1,12 +1,14 @@
|
||||
# PyInstaller's description of the build, shared by the AppImage and the disk
|
||||
# image. Run it through build-appimage.sh or build-dmg.sh rather than by hand:
|
||||
# each of those has a few steps of its own on either side of this.
|
||||
# PyInstaller's description of the build, shared by the AppImage, the disk
|
||||
# image and the Windows setup. Run it through build-appimage.sh, build-dmg.sh
|
||||
# or build-windows.ps1 rather than by hand: each of those has a few steps of
|
||||
# its own on either side of this.
|
||||
#
|
||||
# A directory rather than a single file, on both platforms. Onefile unpacks
|
||||
# itself into /tmp on every start, which for something a global shortcut is
|
||||
# meant to bring up is a second of nothing happening, and for the AppImage it
|
||||
# would be an unpacking inside an unpacking. The single file people download is
|
||||
# the AppImage and the .dmg; this only has to be tidy inside them.
|
||||
# A directory rather than a single file, on all three. Onefile unpacks itself
|
||||
# into a temporary directory on every start, which for something a global
|
||||
# shortcut is meant to bring up is a second of nothing happening, and for the
|
||||
# AppImage it would be an unpacking inside an unpacking. The single file people
|
||||
# download is the AppImage, the .dmg and the setup program; this only has to be
|
||||
# tidy inside them.
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
@@ -24,6 +26,7 @@ __version__ = re.search(r'^__version__ = "(.*)"$',
|
||||
re.M).group(1)
|
||||
|
||||
MACOS = sys.platform == "darwin"
|
||||
WINDOWS = sys.platform == "win32"
|
||||
BUNDLE_ID = "io.github.yusufipk.dikte"
|
||||
|
||||
# PyQt6's wheel is most of the build, and most of the wheel is modules nothing
|
||||
@@ -60,19 +63,41 @@ executable = EXE( # noqa: F821
|
||||
analysis.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name="Dikte" if MACOS else "dikte",
|
||||
name="Dikte" if MACOS or WINDOWS else "dikte",
|
||||
console=False,
|
||||
# Both platforms use whatever the machine is, because neither build is
|
||||
# Every platform uses whatever the machine is, because no build here is
|
||||
# cross-compiled: the workflow runs one job per architecture.
|
||||
target_arch=None,
|
||||
# Ad-hoc, and only on a Mac, where an arm64 binary that carries no
|
||||
# signature at all is refused by the kernel rather than merely warned
|
||||
# about. build-dmg.sh signs the finished bundle over the top of this.
|
||||
codesign_identity="-" if MACOS else None,
|
||||
# Windows keeps the icon inside the executable, and build-windows.ps1 draws
|
||||
# it from the same shapes the tray uses. A Mac reads the one BUNDLE names
|
||||
# below, and the AppImage installs PNGs into the icon theme instead.
|
||||
icon=os.environ.get("DIKTE_ICO") or None,
|
||||
)
|
||||
|
||||
# The same program a second time, as a console application, and only on
|
||||
# Windows. A windowed executable there is one the loader gives no console and
|
||||
# no standard output at all, so `dikte doctor` started from a terminal would
|
||||
# print nothing to it and answer nothing to a script. Everywhere else the one
|
||||
# executable does both jobs: a terminal that started it keeps its output, and
|
||||
# nothing opens a window nobody asked for.
|
||||
console_executable = EXE( # noqa: F821
|
||||
archive,
|
||||
analysis.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name="dikte",
|
||||
console=True,
|
||||
target_arch=None,
|
||||
icon=os.environ.get("DIKTE_ICO") or None,
|
||||
) if WINDOWS else None
|
||||
|
||||
collection = COLLECT( # noqa: F821
|
||||
executable,
|
||||
*([console_executable] if WINDOWS else []),
|
||||
analysis.binaries,
|
||||
analysis.datas,
|
||||
name="dikte",
|
||||
|
||||
+3
-2
@@ -1,11 +1,12 @@
|
||||
"""What the AppImage and the disk image start.
|
||||
"""What the AppImage, the disk image and the Windows setup start.
|
||||
|
||||
dikte/__main__.py is written for a checkout: it puts the directory above the
|
||||
package on the import path, which a build has neither the need for nor a
|
||||
directory to point at. What is left over is one thing a checkout never sees.
|
||||
The Finder hands a double-clicked application a -psn_0_… argument naming the
|
||||
process serial number, which argparse reads as a flag it has never heard of and
|
||||
exits over, and no one clicking an icon would ever find out why.
|
||||
exits over, and no one clicking an icon would ever find out why. Nothing else
|
||||
here is one platform's: the same file is both Windows executables as well.
|
||||
|
||||
The three environment lines have to run before anything starts a process,
|
||||
opens a connection or reaches for ffmpeg, and before is easier to be sure of
|
||||
|
||||
Reference in New Issue
Block a user