Files
dikte/packaging/dikte.spec
T
yusufipek 29643f26b3 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.
2026-08-19 11:11:30 +03:00

138 lines
5.9 KiB
RPMSpec

# 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 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
import re
import sys
ROOT = pathlib.Path(SPECPATH).parent # noqa: F821 (PyInstaller's)
# Read rather than imported. Putting the checkout on sys.path to import dikte
# would put this directory there under the name `packaging`, which is a real
# library that PyInstaller itself uses, and a spec file is no place to find out
# whether that matters.
__version__ = re.search(r'^__version__ = "(.*)"$',
(ROOT / "dikte" / "__init__.py").read_text(),
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
# here imports: Qt ships a browser engine, three declarative UI stacks and a
# 3D renderer. Naming them keeps the download to something a person on a slow
# connection will actually finish. Only the four in dikte's imports are left.
UNUSED_QT = [
"PyQt6." + name for name in (
"Qt3DAnimation", "Qt3DCore", "Qt3DExtras", "Qt3DInput", "Qt3DLogic",
"Qt3DRender", "QtBluetooth", "QtCharts", "QtDataVisualization",
"QtDesigner", "QtHelp", "QtLocation", "QtMultimedia",
"QtMultimediaWidgets", "QtNfc", "QtPdf", "QtPdfWidgets",
"QtPositioning", "QtQml", "QtQuick", "QtQuick3D", "QtQuickWidgets",
"QtRemoteObjects", "QtSensors", "QtSerialPort", "QtSpatialAudio",
"QtSql", "QtTest", "QtTextToSpeech", "QtWebChannel", "QtWebEngineCore",
"QtWebEngineQuick", "QtWebEngineWidgets", "QtWebSockets",
)
]
analysis = Analysis( # noqa: F821
[str(ROOT / "packaging" / "entry.py")],
pathex=[str(ROOT)],
hiddenimports=["PyQt6.QtNetwork"],
# tkinter is the other GUI toolkit CPython ships and would be dead weight;
# dikte's own tests have no business in a build at all.
excludes=UNUSED_QT + ["tkinter", "tests"],
noarchive=False,
)
archive = PYZ(analysis.pure) # noqa: F821
executable = EXE( # noqa: F821
archive,
analysis.scripts,
[],
exclude_binaries=True,
name="Dikte" if MACOS or WINDOWS else "dikte",
console=False,
# 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.
#
# 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-cli",
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",
)
if MACOS:
# LSUIElement is the line that makes this a menu bar application: no Dock
# icon, no menu of its own, nothing in the app switcher. The usage strings
# are not decoration either, they are what the permission dialogs read out,
# and a bundle that asks for the microphone without one is killed rather
# than asked about.
app = BUNDLE( # noqa: F821
collection,
name="Dikte.app",
icon=os.environ.get("DIKTE_ICNS") or None,
bundle_identifier=BUNDLE_ID,
version=__version__,
info_plist={
"CFBundleName": "Dikte",
"CFBundleDisplayName": "Dikte",
"CFBundleShortVersionString": __version__,
"CFBundleVersion": __version__,
"LSMinimumSystemVersion": "11.0",
"LSUIElement": True,
"NSHighResolutionCapable": True,
"NSMicrophoneUsageDescription":
"Dikte records what you dictate so that it can be transcribed.",
"NSAppleEventsUsageDescription":
"Dikte puts the transcript on the clipboard and pastes it into "
"the window you were typing in.",
},
)