ci: build PoxChat for Windows on a runner - #8
Merged
Conversation
The VS solution already knows how to produce both: win32/copy stages a self-contained tree (which is the portable layout, marker file and all) and win32/installer templates poxchat.iss and runs Inno Setup over it. What was missing was a machine to do it on and a way to get the dependencies there. build-deps.ps1 provisions the stack poxchat.props expects: gvsbuild for GTK4, OpenSSL, libxml2, sqlite, luajit, libcurl, enchant and gettext (win32/nls needs its msgfmt), plus jansson and libwebsockets, which gvsbuild has no projects for and which have to be static since DepLibs names the .libs and copy.vcxproj ships no DLLs for either. A cold run compiles GTK for an hour or two; the workflow caches the install prefixes so that happens once per version. CI overrides the props UserMacros from the command line rather than editing them, keeping that file environment-specific as intended. The build is split into named steps so a failure names the piece that broke, and perl, python3 and htm are left out until the rest is green. Nothing here has run yet: MSVC has likely not seen this tree since the GTK4 port.
The first run died two minutes in: pixman comes from cairographics.org, a single machine that refused the connection. With one upstream host per project there is always a chance one of them is down, and losing a cold GTK build to that is not worth it. Retries use --fast-build so they resume rather than start over.
Two things went wrong on the last run. cairographics.org, which serves pixman, redirects to an HTTPS endpoint that is down -- the runner timed out following the redirect, and so does every other network we tried, so the retry added in the last commit was never going to help. Seed the tarball from Debian instead, whose orig file for pixman is the upstream one byte for byte; the sha256 checked here is the hash gvsbuild itself records, so a substituted file fails loudly. gvsbuild is pinned now so that seeded version cannot drift out from under it. The retry itself was broken: splatting two arrays across backtick continuations fed gvsbuild a bare '-', and it exited on the usage error rather than the download. One flat argument array instead.
Same story as pixman, same two dead hosts. cairographics.org serves cairo as well, and icon-theme.freedesktop.org is down the same way, so seed all three from Debian rather than discovering them one failed run at a time. Checked every archive_url in the pinned gvsbuild for hosts that do not answer; these are the ones in our dependency closure. Each hash is what gvsbuild records and what Debian serves.
gvsbuild builds luajit by handing '.\msvcbuild' to CreateProcess, which cannot launch a .bat file and finds nothing under that name without the extension. It failed identically on all three attempts, 45 minutes into a cold stack, right after OpenSSL finished. Nothing on our side can fix that, so luajit is out, and with it lgi (which depends on it), --enable-gi (which exists for lgi's typelibs), and the lua plugin. Re-adding means building LuaJIT here as jansson and libwebsockets already are, and settling what the installer script does about the lua and lgi files it names unconditionally. Meanwhile every failure was costing the next run the three quarters of an hour it had already spent, because the cache only saves on success. Roll it instead: save the whole build tree on any outcome under a per-run key, restore the newest previous one, and always run the dependency step -- gvsbuild's --fast-build skips whatever is already marked built, so a complete stack is a no-op and a partial one resumes.
common's pre-build event runs make-te.py, glib-genmarshal and the config.h template through $(Python3Path), which poxchat.props sets to C:\Python314 -- one more environment-specific path that only exists on a developer's box. The runner's interpreter comes from setup-python and its location is not known until that step has run, so the msbuild steps pass it rather than the workflow-level property block. Nothing else in the targets we build needs it: copy.vcxproj's use is a wildcard that tolerates absence, and the perl and python3 plugins are not in this build.
fe-gtk got all the way to LINK before failing on libcurl.lib: gvsbuild builds libcurl with CMake, which produces libcurl_imp.lib, and it patches libcurl.pc to say -llibcurl_imp for the same reason. DepLibs asks for libcurl.lib, the name curl's own Windows builds use, so copy it to that name the way websockets_static.lib is already handled. That the link reached libcurl at all means jansson.lib and websockets.lib resolved, so the static builds of both are good. Adding an inventory of the prefix so the remaining DepLibs entries can be checked against reality in one run rather than one failed link at a time.
fe-gtk reached LINK for the first time and stopped on seven unresolved Cert* symbols out of websockets.lib. libwebsockets reads the Windows system certificate store from windows-sockets.c, and a static build leaves those imports for whoever links it -- crypt32.lib is theirs, not ours, but it has to be on our link line.
The smoke test proves the staged tree runs, but a missing dependency
shows up as STATUS_DLL_NOT_FOUND with no name attached, so finding the
set costs a runner cycle per file. check-imports.ps1 reads the import
and delay-import tables out of the PE headers directly -- no dumpbin, no
VC environment -- and reports everything unresolved at once, saying
where in the dependency prefix each one can be found.
Three gaps the run 7 inventory already showed, fixed here rather than
waiting for the checker to rediscover them:
sqlite3.dll DepLibs links gvsbuild's import library, not a static
sqlite, so the scrollback store needs the DLL
psl-5.dll gvsbuild builds libcurl against libpsl
libenchant.dll gvsbuild installs it under that name; the entry here
asks for libenchant-2.dll and silently skips
The prefix inventory lists .exe as well now: copy.vcxproj stages the
gspawn helpers unconditionally, so their names matter the same way.
MSVC stopped Build core on fe_get_str: fe.h returns void *, because fe-gtk hands maingui.c back the dialog widget so it can close the prompt itself, and the fe-text stub still returned void. fe_set_batch_mode had no stub at all, and chathistory.c calls it on every batch it replays. Neither shows up on Linux: text-frontend defaults to false in meson_options.txt, so nothing compiles this file there. The Windows solution builds it unconditionally.
fe-text stopped at LINK on jansson.lib. Every project links the same $(DepLibs), but only fe-gtk listed the prefixes that list names -- the rest search $(DepsRoot)\lib alone, which is the gvsbuild prefix and has neither jansson nor libwebsockets in it. fe-gtk linked in run 8 for that reason and nothing else. So the directories belong beside the library list, in the props' own Link defaults, where every project picks them up. Projects keep their own AdditionalLibraryDirectories, which expand ahead of these. This is also waiting for the plugin step: checksum, sysinfo and upd all link $(DepLibs) with the same lone search path fe-text had.
upd.vcxproj stopped on winsparkle-version.h, which winsparkle.h includes and the header copy did not name. Copy the directory winsparkle.h sits in instead: the plugin compiles against whatever the release ships, not against a list of filenames kept here. The rest of the plugin step got through -- checksum, exec, fishlim, sysinfo and winamp all produced DLLs.
PowerShell's for statement takes one repeat expression, not C's comma list, so "$i++, $s += 40" was a parse error and the step never ran. Index off the loop counter instead of carrying a cursor. Verified this time rather than guessed at: pwsh in a container parses both CI scripts clean, and the walker now agrees name for name with an independently written reference implementation across 71 real binaries -- 55 PE32+, 11 PE32, and 5 files that are not PEs at all and are correctly skipped. The all-resolved path exits 0.
The checker's first real pass found two things, one of them its own bug. winspool.drv is a System32 file that gtk-4-1.dll binds to, and the system scan was filtered to *.dll -- .drv, .cpl and .ocx are just as bindable, so drop the filter. The other was real: psl-5.dll wants icuuc78.dll. Staging that alone would only have raised whatever ICU pulls in next, one runner cycle later, which is the trap this script exists to avoid -- so a missing DLL found in the prefix now gets walked too, and the report names the whole chain at once. libcurl -> libpsl -> ICU is a heavy chain for what it does; the ICU DLLs are staged by wildcard because their names carry a major version that a literal would silently stop matching. Checked against pwsh in a container before pushing: transitive results agree name for name with an independent reference over a fixture whose dependencies sit two levels deep, and the flat corpus is unchanged.
…er failure The installer stopped on share\themes\MS-Windows\*, which nothing stages: GTK4 has no theme engine, as the template itself notes twelve lines further down, and gvsbuild's GTK4 ships no such directory. Out of both the installer template and copy.vcxproj. libenchant.dll joins the -2 spelling for the same reason it did in copy.vcxproj -- gvsbuild installs it under the short name, and the installer was shipping without it while the portable zip had it. Also reorder the upload: run 14 built a good portable zip and then threw it away, because the installer step sits between packaging and upload and a failing step skips what follows.
Records the two batch-discovery steps and why they exist, how to run the PowerShell against real PEs on Linux before pushing, what the build leaves out and why, and the traps that cost a runner cycle each the first time -- fe-text having no Linux coverage, DepLibs' search path, the CRLF files, and the ICU chain's size.
The build is green, so the scaffolding that let it be iterated on comes out. Push and pull_request are both master-only now, and the ci/** branch trigger is gone. The dependency cache goes back to what the comment always said it would become: keyed on build-deps.ps1's hash rather than per run, restored and saved by one actions/cache step instead of a restore/save pair that existed to keep partial progress across a failing run. restore-keys stays, so editing the script resumes from the previous stack rather than paying three quarters of an hour for a cold one.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Builds the GTK4 tree for Windows on a GitHub runner and uploads a portable
x64 zip. The last green run produced
PoxChat-3.0.0~alpha2-x64-portable.zip— 39 MiB compressed, both executables, seven plugin DLLs — and ran the
binaries out of the staged tree to prove it.
What's here
.github/workflows/windows-build.ymlplus three PowerShell scripts underwin32/ci/:build-deps.ps1C:\gtk-build— gvsbuild for GTK4/OpenSSL/libxml2/sqlite/libcurl/enchant/gettext, static jansson and libwebsockets, prebuilt WinSparkle, CA bundlecheck-imports.ps1make-installer.ps1poxchat.issfrom its template and runs Inno Setupdocs/areas/windows-ci-build.mdcovers how to work on this without spending arunner cycle per question.
Tree fixes, not just CI plumbing
Getting to green turned up six real defects that had nothing to do with the
runner:
crypt32.libmissing fromDepLibs— a static libwebsockets leaves itsWindows certificate-store imports for whoever links it. This was the last
thing between the tree and a linking
poxchat.exe.fe-text's stubs had drifted fromfe.h—fe_get_strreturnedvoidagainst a
void *prototype, andfe_set_batch_modehad no stub at allwhile
chathistory.ccalls it on every replayed batch.$(DepLibs)' library search path lived only infe-gtk.vcxproj— theother thirteen projects that link the same list could not find jansson or
libwebsockets, which sit outside the gvsbuild prefix. Moved to the props'
own
Linkdefaults, beside the list it belongs to.winsparkle.hincludes.sqlite3.dll,psl-5.dll, the ICU chain andlibenchant.dllwere neverstaged —
sqlite3.libis gvsbuild's import library, not a static build.the staging project; GTK4 has no theme engine.
Two steps that exist to batch discovery
A warm run is ~15 minutes, so anything that learns one fact per run is the
slow path. The prefix inventory lists what gvsbuild actually installed, because
it does not always spell import libraries the way
DepLibsasks for them. Theimport check reads PE import tables directly — Windows reports a missing
dependency as
STATUS_DLL_NOT_FOUNDwith no name attached — and follows amissing DLL into the dependency prefix so a chain like
libcurl.dll -> psl-5.dll -> icuuc78.dllcomes back in one pass.check-imports.ps1was validated against an independently written referenceimplementation over 71 real binaries, 32- and 64-bit, before it was trusted.
Deliberately left out
lua(gvsbuild cannot build LuaJIT — it runs.\msvcbuildthroughCreateProcess, which will not launch a.bat), andperl,python3andhtm, which need matching toolchains on the runner. The installer names thosepayloads unconditionally across
[Files],[Components],[Registry],[Run],[Icons]and the Pascal[Code]section, so-f installer=truefails at Inno Setup until they build. It stays behind its opt-in flag; the
portable zip uploads before that step runs, so nothing else is affected.
Worth knowing
meson_options.txtdefaultstext-frontendtofalse, so nothing on Linuxcompiles
src/fe-text/fe-text.c. The Windows solution builds itunconditionally, which makes this workflow the only thing that notices when
its stubs drift out of step with
src/common/fe.h.The first run on this PR pays for a cold dependency stack — roughly 45–75
minutes — because a pull request run reads caches from its base branch, not
from the topic branch this was iterated on.
🤖 Generated with Claude Code