Keep the two Windows executables from being one file

Windows matches a filename without regard to its case, so Dikte.exe and
dikte.exe were one file in the installation directory: PyInstaller wrote
the console build second and that is the only one that landed. Every
launcher then opened a console window behind the tray, and closing it
killed the application.

The console one is dikte-cli.exe now, which the setup's shim names. The
build reads the PE subsystem of both afterwards, since a filesystem that
keeps the two names apart is exactly what the check would run on
otherwise.
This commit is contained in:
2026-08-19 11:11:30 +03:00
parent 23d56acfb5
commit 29643f26b3
6 changed files with 66 additions and 9 deletions
+22 -2
View File
@@ -45,13 +45,33 @@ $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.
# what a shortcut starts, and dikte-cli.exe, which has a console and is what
# the `dikte` command runs. Their names differ by more than case on purpose;
# Windows would otherwise keep one file for both.
& 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" }
# And the check that the two are really two. Windows matches a filename
# without regard to its case, so a rename that leaves them a case apart puts
# one file in the directory and whichever was written second is what both
# names find. It cost a windowed build once, silently: the machines the
# packaging is otherwise checked on all have case-sensitive filesystems, so
# this only ever shows up here. Subsystem 2 is windowed, 3 is a console, and
# it is the field the loader reads to decide which to give.
function Get-PeSubsystem($path) {
$bytes = [System.IO.File]::ReadAllBytes($path)
$header = [BitConverter]::ToInt32($bytes, 0x3C)
return [BitConverter]::ToUInt16($bytes, $header + 0x5C)
}
foreach ($pair in @(@("Dikte.exe", 2), @("dikte-cli.exe", 3))) {
$subsystem = Get-PeSubsystem (Join-Path $dist $pair[0])
if ($subsystem -ne $pair[1]) {
throw "$($pair[0]) is subsystem $subsystem, expected $($pair[1]): the two executables collided"
}
}
# 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
+3 -3
View File
@@ -84,8 +84,8 @@ Filename: "{app}\Dikte.exe"; Description: "Start Dikte"; \
{ 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. }
open terminal misses. It names dikte-cli.exe, the console executable, which
is the one that can print to the terminal it was typed in. }
function ShimDir(): String;
begin
@@ -104,7 +104,7 @@ begin
if CurStep = ssPostInstall then begin
if DirExists(ShimDir()) then begin
Shim := '@echo off' + #13#10
+ '"' + ExpandConstant('{app}\dikte.exe') + '" %*' + #13#10;
+ '"' + ExpandConstant('{app}\dikte-cli.exe') + '" %*' + #13#10;
SaveStringToFile(ShimPath(), Shim, False);
end;
end;
+6 -1
View File
@@ -84,12 +84,17 @@ executable = EXE( # noqa: F821
# 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.
#
# Named apart from the windowed one rather than `dikte` beside `Dikte`.
# Windows matches a filename without regard to its case, so those two are one
# file in one directory: whichever PyInstaller writes second is the only one
# installed, and a build made on a case-sensitive filesystem never sees it.
console_executable = EXE( # noqa: F821
archive,
analysis.scripts,
[],
exclude_binaries=True,
name="dikte",
name="dikte-cli",
console=True,
target_arch=None,
icon=os.environ.get("DIKTE_ICO") or None,