Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 191 additions & 0 deletions .github/workflows/windows-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
name: Windows Build

on:
push:
branches: [master]
pull_request:
branches: [master]
workflow_dispatch:
inputs:
installer:
description: Also build the Inno Setup installer
type: boolean
default: false

permissions:
contents: read

concurrency:
group: windows-${{ github.ref }}
cancel-in-progress: true

defaults:
run:
shell: pwsh

env:
# Bump to invalidate the dependency cache without editing build-deps.ps1 --
# e.g. when a new gvsbuild release should be picked up.
DEPS_CACHE_EPOCH: '1'
BUILD_DIR: ${{ github.workspace }}\poxchat-build

# poxchat.props keeps its UserMacros environment-specific on purpose, so CI
# overrides them from the command line rather than editing the file.
# YourPython3Path is not here: common's pre-build event runs make-te.py and
# glib-genmarshal through it, and the interpreter's path is only known once
# setup-python has run, so each msbuild step passes it itself.
# PoxChatBuild moves the output tree inside the workspace; its default puts it
# a level above the checkout.
MSBUILD_PROPS: >-
/nologo /m /v:minimal
/p:Configuration=Release
/p:Platform=x64
/p:PoxChatBuild=${{ github.workspace }}\poxchat-build
/p:YourDepsPath=C:\gtk-build\gtk
/p:YourOpenSSLPath=C:\gtk-build\gtk\x64\release
/p:YourLibCurlPath=C:\gtk-build\gtk\x64\release
/p:YourJanssonPath=C:\gtk-build\jansson
/p:YourLibWebSocketsPath=C:\gtk-build\libwebsockets
/p:YourWinSparklePath=C:\gtk-build\WinSparkle
/p:YourCACertPath=C:\gtk-build\cert\cacert.pem

jobs:
build:
runs-on: windows-2022
timeout-minutes: 240

steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- uses: actions/setup-python@v5
with:
python-version: '3.12'

# The whole C:\gtk-build tree: install prefix, intermediates and source
# archives. The intermediates belong in here because gvsbuild's
# --fast-build decides what to skip by reading .wingtk-built markers out
# of the build tree, not out of the prefix.
#
# A cold stack is three quarters of an hour, so restore-keys lets an
# edited build-deps.ps1 resume from the previous one rather than pay that
# again; bump DEPS_CACHE_EPOCH for a deliberate clean slate.
- name: Dependency stack
uses: actions/cache@v4
with:
path: C:\gtk-build
key: win-deps-${{ env.DEPS_CACHE_EPOCH }}-${{ hashFiles('win32/ci/build-deps.ps1') }}
restore-keys: |
win-deps-${{ env.DEPS_CACHE_EPOCH }}-

# Always run, never skipped on a cache hit: with a complete stack restored
# this is a quick no-op, and it is the only thing that can tell a complete
# stack from a half-built one -- a partial hit through restore-keys is
# exactly that.
- name: Build dependency stack
run: .\win32\ci\build-deps.ps1

# poxchat.props names the import libraries it wants in DepLibs, and
# gvsbuild does not always spell them the same way -- listing what is
# actually in the prefix turns a one-name-per-run guessing game into a
# single diff against that list.
- name: Inventory the dependency prefix
run: |
$prefix = 'C:\gtk-build\gtk\x64\release'
Write-Host '--- lib ---'
(Get-ChildItem "$prefix\lib\*.lib" | ForEach-Object Name) -join ' '
Write-Host '--- bin ---'
(Get-ChildItem "$prefix\bin\*.dll", "$prefix\bin\*.exe" | ForEach-Object Name) -join ' '

- uses: microsoft/setup-msbuild@v2

- name: Read version
id: version
run: |
$line = Select-String -Path meson.build -Pattern "^\s+version: '([^']+)'," | Select-Object -First 1
if (-not $line) { throw 'no version in meson.build' }
"version=$($line.Matches[0].Groups[1].Value)" >> $env:GITHUB_OUTPUT

# Split by target so a failure names the piece that broke. perl, python3
# and htm (the C# theme manager) are left out until the rest is green:
# they need a matching Strawberry Perl / CPython / .NET on the runner.
# lua is out too, for want of a LuaJIT gvsbuild can build -- see
# build-deps.ps1.
- name: Build core
run: msbuild win32\poxchat.sln ${{ env.MSBUILD_PROPS }} /p:YourPython3Path=$env:pythonLocation /t:"poxchat\common;poxchat\fe-gtk;poxchat\fe-text"

- name: Build plugins
run: msbuild win32\poxchat.sln ${{ env.MSBUILD_PROPS }} /p:YourPython3Path=$env:pythonLocation /t:"plugins\checksum;plugins\exec;plugins\fishlim;plugins\sysinfo;plugins\winamp;plugins\upd;plugins\notifications-winrt;external\libenchant_win8"

- name: Stage the distributable tree
run: msbuild win32\poxchat.sln ${{ env.MSBUILD_PROPS }} /p:YourPython3Path=$env:pythonLocation /t:"release\nls;release\copy"

# The smoke test below proves the tree runs, but STATUS_DLL_NOT_FOUND does
# not say which DLL is missing, so name the whole set first -- same
# reasoning as the inventory step: one runner cycle per missing file is
# the slow way to find them.
- name: Check the staged tree's imports
run: >-
.\win32\ci\check-imports.ps1
-Root "${{ env.BUILD_DIR }}\x64\rel"
-Prefix C:\gtk-build\gtk\x64\release\bin,C:\gtk-build\WinSparkle\x64

# The staged tree is what ships, so run the binaries from it: a missing
# DLL kills the process immediately with STATUS_DLL_NOT_FOUND (0xC0000135)
# rather than showing up later as a user unzipping a broken build.
#
# poxchat-text.exe --version prints and exits. poxchat.exe --version puts
# the string in a modal dialog on Windows, so surviving past gtk_init is
# the pass condition there, and we kill it afterwards.
- name: Smoke test the staged tree
run: |
$rel = "${{ env.BUILD_DIR }}\x64\rel"

$text = Join-Path $rel 'poxchat-text.exe'
if (-not (Test-Path $text)) { throw 'poxchat-text.exe missing from the staged tree' }
& $text --version
if ($LASTEXITCODE -ne 0) {
throw ("poxchat-text.exe --version exited with 0x{0:X8}" -f $LASTEXITCODE)
}

$gui = Join-Path $rel 'poxchat.exe'
if (-not (Test-Path $gui)) { throw 'poxchat.exe missing from the staged tree' }
$proc = Start-Process -FilePath $gui -ArgumentList '--version' -PassThru
if ($proc.WaitForExit(30000)) {
if ($proc.ExitCode -ne 0) {
throw ("poxchat.exe --version exited with 0x{0:X8}" -f $proc.ExitCode)
}
} else {
Write-Host 'poxchat.exe reached its version dialog; killing it'
$proc.Kill()
}

# copy.vcxproj already drops a portable-mode marker in the tree, which is
# what makes it keep its config beside the binary.
- name: Package portable zip
id: portable
run: |
$rel = "${{ env.BUILD_DIR }}\x64\rel"
$name = "PoxChat-${{ steps.version.outputs.version }}-x64-portable.zip"
$out = Join-Path "${{ github.workspace }}" $name
7z a -tzip -mx=7 "$out" "$rel\*" | Out-Null
if ($LASTEXITCODE -ne 0) { throw "7z failed with exit code $LASTEXITCODE" }
"path=$out" >> $env:GITHUB_OUTPUT

- name: Upload portable build
uses: actions/upload-artifact@v4
with:
name: poxchat-x64-portable
path: ${{ steps.portable.outputs.path }}

- name: Build installer
if: inputs.installer
run: .\win32\ci\make-installer.ps1 -BuildDir "${{ env.BUILD_DIR }}" -Provision

- name: Upload installer
if: inputs.installer
uses: actions/upload-artifact@v4
with:
name: poxchat-x64-installer
path: ${{ env.BUILD_DIR }}\x64\*.exe
79 changes: 0 additions & 79 deletions .github/workflows/windows-build.yml.disabled

This file was deleted.

102 changes: 102 additions & 0 deletions docs/areas/windows-ci-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Windows CI build

How `.github/workflows/windows-build.yml` and `win32/ci/` fit together, and
how to iterate on them without spending a runner cycle per question. Useful
when the Windows build breaks, when a dependency version moves under it, or
when re-enabling one of the targets it currently leaves out.

## Shape

| Piece | Does |
|-------|------|
| `win32/ci/build-deps.ps1` | Builds the whole dependency stack into `C:\gtk-build` — gvsbuild for GTK4/OpenSSL/libxml2/sqlite/libcurl/enchant/gettext, then static jansson and libwebsockets, prebuilt WinSparkle, and the CA bundle |
| `win32/ci/check-imports.ps1` | Walks PE imports of the staged tree and names every DLL it is missing |
| `win32/ci/make-installer.ps1` | Generates `poxchat.iss` from its template and runs Inno Setup 5 |

A cold dependency stack is about three quarters of an hour; warm, a run is
roughly 15 minutes. That ratio is why `C:\gtk-build` is cached whole and keyed
on `build-deps.ps1`'s hash, with a `restore-keys` prefix so that editing the
script resumes from the previous stack instead of rebuilding it. Bump
`DEPS_CACHE_EPOCH` when you want a deliberate clean slate.

Cache scope is worth knowing: a pull request run can read caches from its base
branch, not from the topic branch the work was done on, so the first PR run
after a branch has been iterating in isolation pays for a cold stack.

## The rule that matters: never learn one fact per run

At 15 minutes a cycle, anything that discovers problems one at a time is the
slow path. Two steps exist purely to batch that discovery, and both were added
after paying the toll:

- **Inventory the dependency prefix** lists every `.lib` and `.exe`/`.dll` in
the gvsbuild prefix. `poxchat.props`'s `DepLibs` names import libraries, and
gvsbuild does not always spell them the same way — `libcurl_imp.lib` vs
`libcurl.lib`, `libenchant.dll` vs `libenchant-2.dll`. Diff the list against
what the build asks for instead of guessing one name per run.
- **Check the staged tree's imports** reads the PE import and delay-import
tables directly. Windows reports a missing dependency as
`STATUS_DLL_NOT_FOUND` (0xC0000135) at process start with no name attached,
so the smoke test can only say *broken*. When a missing DLL turns up in the
dependency prefix the checker follows it and walks its imports too, so a
chain like `libcurl.dll -> psl-5.dll -> icuuc78.dll` is reported in one pass.

The same instinct applies off the runner. Every `Source:` line in
`win32/installer/poxchat.iss.tt` can be checked against a downloaded portable
zip locally; Inno Setup aborts on the first one that matches nothing, so
auditing all 72 at once is the difference between one cycle and fourteen.

## Validating the PowerShell without Windows

Both CI scripts can be exercised on Linux, which is worth doing — a bare
syntax error in a late step costs a full run:

```sh
# parse check
docker run --rm -v "$PWD/win32/ci:/ci:ro" mcr.microsoft.com/powershell \
pwsh -NoProfile -Command '$e=$null;$t=$null
[System.Management.Automation.Language.Parser]::ParseFile("/ci/check-imports.ps1",[ref]$t,[ref]$e)|Out-Null
if($e){$e|%{"{0}: {1}" -f $_.Extent.StartLineNumber,$_.Message}}else{"ok"}'

# behaviour check -- point it at any directory of real PE files
docker run --rm -v "$PWD/win32/ci:/ci:ro" -v /some/dlls:/tree:ro \
mcr.microsoft.com/powershell pwsh -NoProfile -File /ci/check-imports.ps1 -Root /tree
```

`check-imports.ps1` runs fine on Linux: `$env:SystemRoot` is empty there, so
the System32 scan is skipped and every system DLL reports as missing, which is
harmless when what you are checking is the set of imports it discovers. A wine
prefix is a convenient corpus of real 32- and 64-bit PEs to test against.

Watch for PowerShell-vs-C reflexes. `for ($i = 0; $i -lt $n; $i++, $p += 40)`
is a parse error: the repeat clause takes one expression, not a comma list.

## Currently left out

- **lua** — gvsbuild builds LuaJIT by running `.\msvcbuild` through
`CreateProcess`, which cannot launch a `.bat` and finds nothing without the
extension. Taking lua back means building LuaJIT here the way jansson and
libwebsockets are built, and lgi and `--enable-gi` come with it.
- **perl, python3, htm** — need a matching Strawberry Perl / CPython / .NET on
the runner.
- **The installer.** `poxchat.iss.tt` names the lua, perl, python and
thememan payloads unconditionally, across `[Files]`, `[Components]`,
`[Registry]`, `[Run]`, `[Icons]` and the Pascal `[Code]` section. Until
those targets build, `-f installer=true` fails at Inno Setup. The portable
zip uploads before the installer step runs, so this costs nothing else.

## Traps worth remembering

- `meson_options.txt` defaults `text-frontend` to **false**, so nothing on
Linux compiles `src/fe-text/fe-text.c`. The Windows solution builds it
unconditionally, which makes a Windows CI run the only thing that notices
when its stubs drift out of step with `src/common/fe.h`.
- Every project links the same `$(DepLibs)`, so the search path for it belongs
in the props' own `Link` defaults, not per project. jansson and libwebsockets
live outside the gvsbuild prefix that `$(DepsRoot)\lib` points at.
- `win32/poxchat.props` and `win32/copy/copy.vcxproj` are CRLF. Edit them as
bytes, or a tool that rewrites line endings turns a two-line change into a
whole-file diff.
- ICU is 35 MB of the staged tree, reached only as
`libcurl -> libpsl -> ICU`, and libpsl is there for public-suffix checks on
cookies. Dropping it means building libcurl without libpsl in gvsbuild.
4 changes: 3 additions & 1 deletion src/fe-text/fe-text.c
Original file line number Diff line number Diff line change
Expand Up @@ -855,9 +855,10 @@ void
fe_get_bool (char *title, char *prompt, void *callback, void *userdata)
{
}
void
void *
fe_get_str (char *prompt, char *def, void *callback, void *ud)
{
return NULL;
}
void
fe_get_int (char *prompt, int def, void *callback, void *ud)
Expand Down Expand Up @@ -975,5 +976,6 @@ void fe_scrollback_set_virtual (struct session *sess, void *db, const char *chan
int total_entries, gint64 max_rowid) {}
void fe_set_pending_db_rowid (struct session *sess, gint64 rowid) {}
void fe_resolve_pending_dup (struct session *sess, gint64 old_rowid, gint64 new_rowid) {}
void fe_set_batch_mode (struct session *sess, gboolean on) {}
void fe_begin_multiline_group (struct session *sess) {}
void fe_end_multiline_group (struct session *sess) {}
Loading
Loading