Everything in this directory describes how the Windows XP build is produced and
verified. It lives in the repository for the same reason patches/ does: a clone
that has the sources and the submodule patches still cannot reproduce the binary
without the FFmpeg configuration and the pre-deploy checks that follow.
| file | what it is |
|---|---|
build_ffmpeg_xp.sh |
the FFmpeg (release/3.4) static-lib recipe: which encoders, decoders, demuxers, muxers and filters this port needs, and why |
xpsafe.ps1 |
pre-deploy check of Telegram.exe: no absent-on-XP DLL imported NORMAL, and no Vista+ function entry point imported |
ffopus_xp.cpp, build_ffopus.bat |
harness that links the real FFmpeg libs, prints an inventory and reproduces the voice-message encode path |
xp_env.ps1 |
the single definition of what "building for XP" means — INCLUDE/LIB/PATH, and the -Arch switch every other script defers to |
bootstrap_toolchain.ps1 |
builds the toolchain on a machine that has none (a CI runner): the v141 target, SDK 7.1A, the patched headers, fpcompat, xp_compat |
build_libraries.ps1, build_qt.ps1 |
the pinned third-party set and the static Qt, per architecture |
fpcompat/xpfls.asm, fpcompat/xpfls64.asm |
the CRT's Vista+ import slots pointed at XP equivalents — 32- and 64-bit dialects of the same six thunks |
exports/, exports-x64-extra.txt |
the export whitelist xpsafe.ps1 checks against, and the provisional 64-bit overlay |
Machine-specific locations (Libraries-walk, fpcompat, the v141_xp environment)
are variables with the current workspace as the default — override them from the
environment rather than editing the scripts.
The build uses the 14.44 compiler binary with a v141_xp 14.16 target via
INCLUDE/LIB, a patched SDK 7.1A include tree, /d2FH4- (FH3 exception
handling, which is what the 14.16 CRT provides) and /SUBSYSTEM:WINDOWS,5.01
(5.02 on x64 — see below). The workspace wrapper cmake_xp.ps1 injects that
environment; never build with a bare ninja.
| x86 | x64 | |
|---|---|---|
| Windows | XP SP3 | XP Professional x64 Edition |
| NT version | 5.1 | 5.2 — the Server 2003 kernel |
| subsystem | 5.01 | 5.02 (5.01 is not a legal value for an x64 image and the linker refuses it) |
| package | txpupd<version> |
txp64upd<version> |
| feed key | winxp |
winxp64 |
Everything is chosen by one switch: -Arch x86|x64 on each script, defaulting to
$env:XP_ARCH, which xp_env.ps1 also exports along with XP_SUBSYSTEM_VERSION,
XP_MSVC_PLATFORM and XP_MACHINE. Set XP_ARCH once and the rest follows.
powershell xp\bootstrap_toolchain.ps1 -Root C:\xp-toolchain -Arch x64
powershell xp\build_libraries.ps1 -Root C:\xp-toolchain\Libraries-x64 -Arch x64
powershell xp\build_qt.ps1 -Root C:\xp-toolchain -Arch x64
Shared between them: the toolset install (one component carries both targets), the
patched 7.1A include tree, and the qt5-xp source checkout. Per-architecture, in
sibling directories with a -x64 suffix so nothing already built moves:
fpcompat, xp_compat, Libraries, qt-xp-static-prefix, the build tree, and
every CI cache key.
One thing does not move: cmake/variables.cmake resolves the dependencies as
../Libraries-walk, a sibling of the source tree, and that rule is the same for
both. The release workflow satisfies it with a junction whose target changes
with the architecture — the name stays put. A workstation building x64 has to do
the same (repoint ..\Libraries-walk at the x64 set) or keep a second checkout;
the two cannot be configured side by side out of one tree.
What actually differs in the code, beyond library paths:
- fpcompat has no float helpers on x64.
ftol2/ftol3and__ltof3/__ultof3/__dtoul3_legacyanswer calls the 32-bit compiler emits for double→integer conversion; the 64-bit one does it inline with SSE2 and emits none of them, solib\x64\libcmt.libhas no such members and there is nothing to lift out.fpcompat_host.libwould then be an empty archive, whichlib.exerefuses to write —fpcompat/hoststub.cis the one object that fills it. - The FLS thunks are assembled from
fpcompat/xpfls64.asmbyml64, because x64 has no stdcall decoration (__imp_FlsAlloc, not__imp__FlsAlloc@4) and an import slot is 8 bytes. The C half (xpfls.c) is shared. Note that NT 5.2 does exportFls*andGetNumaHighestNodeNumber—InitializeCriticalSectionExis the one that is genuinely Vista+ on both — but they are all thunked anyway, so the binary does not depend on which service pack the target carries. - OpenSSL configures as
VC-WIN64A. The output directories do not change with it: mk1mf still writesout32/inc32, which is whatcmake/external/opensslpoints at for both. - FFmpeg needs no flag at all. Its configure decides the subarch by compiling a
_M_X64probe with whatevercl.exeis on PATH, so it follows the environment. - Qt uses the
win32-msvcmkspec on both. There is nowin64-msvc; Qt 5 takes the architecture from the compiler.
xp/exports-x64 holds the real export tables of a Windows XP Professional x64
Edition install — 34 DLLs, 10 792 names, against which xpsafe.ps1 -Arch x64
checks every named import. Same contract as the 32-bit xp/exports.
They come off XP x64 RTM, 5.2.3790.1830 (srv03_sp1_rtm). If the machines
this ships to run SP2, the tables are a subset of what they have, which errs the
safe way — the gate can report a name that exists there, never wave through one
that does not.
Why this matters is not theoretical. Every 64-bit binary imports the x64 SEH
unwinder — RtlLookupFunctionEntry, RtlPcToFileHeader, RtlUnwindEx,
RtlVirtualUnwind — and 32-bit Windows has no counterpart for any of them, so
without these tables the gate fails a perfectly good build on its very first probe.
A tree without xp/exports-x64 falls back to the x86 tables and prints
PROVISIONAL rather than refusing to run; see the header of xpsafe.ps1.
To re-dump, the guest half needs Guest Additions, which 7.2.6 does not deliver on
NT 5.2 x64 — the services start and the VBoxGuest driver does not, so
guestcontrol and shared folders are both dead. Read the DLLs off the disk instead,
with no guest running and no elevation:
VBoxManage clonemedium disk WinXP64.vdi xp64.img --format RAW # XP puts NTFS at LBA 63
$T = 'C:\Users\h\xp-iso\win81-deploy\tools\ntfs-reader.ps1'
& $T -Image xp64.img -PartLBA 63 -Paths 'WINDOWS\system32\kernel32.dll',... -MaxBytes 20MB -To dlls
& $T -Image xp64.img -PartLBA 63 -Find 'gdiplus.dll' # take the amd64_ WinSxS one
powershell xp\dump_xp_exports.ps1 -Arch x64 -From dllssystem32 on that machine holds the 64-bit DLLs; SysWOW64 holds the 32-bit set,
which a 64-bit binary never imports. When the guest can run commands, the shorter
route is xp\dump_xp_exports.ps1 -Arch x64, which drives the VM through
xpvm-tools\xpexec.ps1 -Vm WinXP64 by itself.
1. A Vista+ entry point. xpsafe.ps1 used to check DLLs only. kernel32.dll
exists on XP, so a build importing ReleaseSRWLockExclusive passed the check and
then died on the machine with "The procedure entry point ... could not be
located" — before main(). The culprit was std::shared_mutex, which MSVC
implements on the SRW lock API. Prefer std::mutex + std::condition_variable in
anything that reaches this link.
The check is now a whitelist: xp/exports/*.txt are the real export tables of
the target XP SP3 image (34 DLLs, ~10 700 names, produced by dump_xp_exports.ps1
off the VM), and every named import of the finished binary must appear in them. A
blacklist only ever catches what has already broken once; this catches the next one
too. Re-dump only when the gate reports a NORMAL dependency with no table.
2. Stale FFmpeg objects after a configure change. With the msvc toolchain
ffmpeg's header dependency tracking does not notice config.h changing, so
allcodecs.o / allformats.o keep the previous component list: the libraries
contain the new codecs while avcodec_register_all() registers none of them, and
config.h looks perfect. make clean is therefore part of the recipe. Verify with
the harness, not by reading config.h.
3. Registration is per-library. FFmpeg 3.4 needs av_register_all(),
avcodec_register_all() and avfilter_register_all(); tdesktop targets a
version where all three are gone, so it calls none of them. They live in
FFmpeg::EnsureRegistered() (ffmpeg_utility.cpp) and must be reached before the
first use of each library — including the cached probe in
Media::Audio::SupportsSpeedControl().
The checks above prove the binary can start on XP. They cannot see the other half
of this port's history: FFmpeg 3.4 registering nothing, libopus refusing FLTP,
rlottie deadlocking when a chat opens, an image plugin dropping out. Those only ever
showed up in front of the VM, one human at a time.
-xpselftest runs those subsystems and returns an exit code: 0 all probes passed
or were skipped, 1 a probe failed, 3 a probe hung and the watchdog killed the
process (the deadlock case — the reason a watchdog exists at all). Each line of the
report names the stage, the status and the timing; the file is appended as it goes,
so a crash still shows which probe was running.
It runs before the launcher, on a bare QCoreApplication: no window, no account,
no working directory. That is what lets the CI runner execute it seconds after the
link — and most of what it catches is not XP-specific at all, so it does not have to
wait for the VM. Probes that need the platform (OpenAL) report SKIP elsewhere.
Every runtime bug found from here on belongs in xp_selftest_win.cpp as a probe.
That is the whole point: the live run stops being how regressions are found.
The XP build updates itself through the same MTProto feed as the fork's Windows 7+ and Linux releases, and stays separate inside it:
- the client asks the feed for the
winxpkey -winxp64on the 64-bit build (update_checker.cpp, notPlatform::AutoUpdateKey(), which sayswinfor any x86 build) - so a Windows 7+ package, which cannot even start here, is never offered. The two XP builds are kept apart for a reason of their own: the x86 one runs on XP x64 through WOW64, so a shared key would silently move a 64-bit machine onto the 32-bit line and keep it there; - packages are named
txpupd<version>(Packer -target winxp) andtxp64upd<version>(-target winxp64) for the same reason, andFindUpdateFile()accepts both prefixes; - the signature key pair is the fork's. It lives in TWO places that must agree:
config.h(client verifies) and_other/packer.cpp(Packer verifies its own output). A mismatch fails at packing time, which is the good outcome.
Publishing is xp/publish_telegram.py from the release workflow: it uploads the
package to the files channel and MERGES a winxp / winxp64 entry into the feed
message, leaving every other platform's entry untouched. It publishes whichever of
the two packages the run produced — both, when the matrix built both — in one
message. publish_update: rehearsal sends
everything 360 days into the future - the whole path runs, nothing appears yet.
Enabling this needs DESKTOP_APP_DISABLE_AUTOUPDATE=OFF (the port has no
DESKTOP_APP_SPECIAL_TARGET, which is what upstream keys autoupdate off) plus
DESKTOP_APP_BUILD_PACKER=ON, and Updater.exe must ship next to the app - it is
what replaces the running binary, so xpsafe gates it exactly like Telegram.exe.
build_ffmpeg_xp.sh # only when the FFmpeg configuration changes
cmake_xp.ps1 ninja ... Telegram
xp/xpsafe.ps1 # must print PASS -- both sections
Telegram.exe -xpselftest # the runtime half; also runs on the CI runner
xpdeploy_keep.ps1 # preserves tdata, so a real chat can be opened
xpalive.ps1 # a screenshot is NOT proof the build survives