diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml new file mode 100644 index 00000000..a8a0696d --- /dev/null +++ b/.github/workflows/windows-build.yml @@ -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 diff --git a/.github/workflows/windows-build.yml.disabled b/.github/workflows/windows-build.yml.disabled deleted file mode 100644 index b50c5c9f..00000000 --- a/.github/workflows/windows-build.yml.disabled +++ /dev/null @@ -1,79 +0,0 @@ -# Disabled during PoxChat rename / GTK4 migration. Re-enable once builds work. -# -# name: Windows Build -# on: -# push: -# branches: -# - master -# pull_request: -# branches: -# - master -# -# jobs: -# windows_build: -# runs-on: windows-2019 -# strategy: -# matrix: -# platform: [x64, win32] -# arch: [x64, x86] -# exclude: -# - platform: x64 -# arch: x86 -# - platform: win32 -# arch: x64 -# fail-fast: false -# -# steps: -# - uses: actions/checkout@v4 -# with: -# submodules: recursive -# -# - name: Install Dependencies -# run: | -# New-Item -Name "deps" -ItemType "Directory" -# -# Invoke-WebRequest http://files.jrsoftware.org/is/5/innosetup-5.5.9-unicode.exe -OutFile deps\innosetup-unicode.exe -# & deps\innosetup-unicode.exe /VERYSILENT | Out-Null -# -# Invoke-WebRequest https://github.com/hexchat/gvsbuild/releases/download/hexchat-2.16.2/idpsetup-1.5.1.exe -OutFile deps\idpsetup.exe -# & deps\idpsetup.exe /VERYSILENT -# -# Invoke-WebRequest https://github.com/hexchat/gvsbuild/releases/download/hexchat-2.16.2/gtk-${{ matrix.platform }}-2018-08-29-openssl1.1.7z -OutFile deps\gtk-${{ matrix.arch }}.7z -# & 7z.exe x deps\gtk-${{ matrix.arch }}.7z -oC:\gtk-build\gtk -# -# Invoke-WebRequest https://github.com/hexchat/gvsbuild/releases/download/hexchat-2.16.2/gendef-20111031.7z -OutFile deps\gendef.7z -# & 7z.exe x deps\gendef.7z -oC:\gtk-build -# -# Invoke-WebRequest https://github.com/hexchat/gvsbuild/releases/download/hexchat-2.16.2/WinSparkle-20151011.7z -OutFile deps\WinSparkle.7z -# & 7z.exe x deps\WinSparkle.7z -oC:\gtk-build\WinSparkle -# -# Invoke-WebRequest https://github.com/hexchat/gvsbuild/releases/download/hexchat-2.16.2/perl-5.20.0-${{ matrix.arch }}.7z -OutFile deps\perl-${{ matrix.arch }}.7z -# & 7z.exe x deps\perl-${{ matrix.arch }}.7z -oC:\gtk-build\perl-5.20\${{ matrix.platform }} -# -# New-Item -Path "c:\gtk-build" -Name "python-3.8" -ItemType "Directory" -# New-Item -Path "c:\gtk-build\python-3.8" -Name "${{ matrix.platform }}" -ItemType "SymbolicLink" -Value "C:/hostedtoolcache/windows/Python/3.8.10/${{ matrix.arch }}" -# -# C:/hostedtoolcache/windows/Python/3.8.10/${{ matrix.arch }}/python.exe -m pip install cffi -# shell: powershell -# -# - name: Build -# run: | -# call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\Tools\VsDevCmd.bat" -# msbuild win32\poxchat.sln /m /verbosity:minimal /p:Configuration=Release /p:Platform=${{ matrix.platform }} -# shell: cmd -# -# - name: Preparing Artifacts -# run: | -# move ..\poxchat-build\${{ matrix.platform }}\PoxChat*.exe .\ -# move ..\poxchat-build .\ -# shell: cmd -# -# - uses: actions/upload-artifact@v2 -# with: -# name: Installer ${{ matrix.arch }} -# path: PoxChat*.exe -# -# - uses: actions/upload-artifact@v2 -# with: -# name: Build Files ${{ matrix.arch }} -# path: poxchat-build diff --git a/docs/areas/windows-ci-build.md b/docs/areas/windows-ci-build.md new file mode 100644 index 00000000..c1098f68 --- /dev/null +++ b/docs/areas/windows-ci-build.md @@ -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. diff --git a/src/fe-text/fe-text.c b/src/fe-text/fe-text.c index c74479de..aacf4442 100644 --- a/src/fe-text/fe-text.c +++ b/src/fe-text/fe-text.c @@ -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) @@ -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) {} diff --git a/win32/ci/build-deps.ps1 b/win32/ci/build-deps.ps1 new file mode 100644 index 00000000..c04a3ec7 --- /dev/null +++ b/win32/ci/build-deps.ps1 @@ -0,0 +1,274 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Build the Windows dependency stack that win32/poxchat.props expects. + +.DESCRIPTION + Everything lands under -BuildRoot (default C:\gtk-build), in the layout the + solution's UserMacros point at: + + gtk\\release gvsbuild prefix: GTK4, OpenSSL, libxml2, sqlite, + luajit, libcurl, enchant, and gettext (win32\nls + compiles the .po catalogues with its msgfmt.exe) + jansson\ static jansson -- DepLibs wants jansson.lib and + copy.vcxproj ships no jansson dll + libwebsockets\ static libwebsockets, same reasoning + WinSparkle\\ prebuilt release, for the upd plugin + cert\cacert.pem CA bundle, shipped into the tree as cert.pem + + Nothing outside -BuildRoot is written, so this is safe to run on a developer + box as well as in CI. It is not incremental beyond gvsbuild's own caching: + CI restores the whole tree from actions/cache instead. +#> + +[CmdletBinding()] +param ( + [string] $BuildRoot = 'C:\gtk-build', + [ValidateSet('x64')] + [string] $Platform = 'x64', + # Pinned rather than floating: $SeededArchives below names an exact pixman + # tarball, and that has to match the version this gvsbuild asks for. Bump + # them together. + [string] $GvsbuildVersion = '2026.8.0', + [string] $JanssonTag = 'v2.15.1', + [string] $LibWebSocketsTag = 'v4.5.8', + [string] $WinSparkleVersion = '0.9.4', + [string] $CMakeGenerator = 'Visual Studio 17 2022' +) + +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' # Invoke-WebRequest crawls with the progress bar on + +# gvsbuild project names, not pkg-config names. gettext is here for msgfmt.exe, +# which win32\nls compiles the .po catalogues with. +# +# luajit is absent, and with it lgi (which depends on it) and --enable-gi (which +# exists to produce lgi's typelibs). gvsbuild builds luajit by running +# '.\msvcbuild' through CreateProcess, which cannot launch a .bat file and finds +# nothing without the extension -- it fails identically every time, so the lua +# plugin is out of the first build. Re-adding it means building LuaJIT here the +# way jansson and libwebsockets are built below; the installer script also names +# lua and lgi files unconditionally, so that has to be settled at the same time. +$GvsbuildProjects = @( + 'gtk4', + 'openssl', + 'libxml2', + 'sqlite', + 'libcurl', + 'enchant', + 'gettext' +) + +# gvsbuild pulls each project's tarball from that project's own upstream, and +# two of those upstreams are simply down: cairographics.org (cairo, pixman) and +# icon-theme.freedesktop.org (hicolor-icon-theme) both time out on connect, from +# the runners and from every other network tried. Retrying cannot revive a host +# that is off, so seed those archives instead. +# +# Debian's orig tarballs are the upstream files byte for byte: every hash below +# is the one gvsbuild itself records for that version, checked against what +# Debian serves. We verify the digest after download too, so a substituted file +# fails loudly rather than quietly building something nobody vetted. +# +# These pin exact versions, which is why $GvsbuildVersion is pinned as well -- +# bump them together, and drop an entry once its upstream comes back. +$SeededArchives = @( + @{ + Name = 'pixman-0.46.4.tar.gz' + Url = 'http://deb.debian.org/debian/pool/main/p/pixman/pixman_0.46.4.orig.tar.gz' + Sha256 = 'd09c44ebc3bd5bee7021c79f922fe8fb2fb57f7320f55e97ff9914d2346a591c' + }, + @{ + Name = 'cairo-1.18.4.tar.xz' + Url = 'http://deb.debian.org/debian/pool/main/c/cairo/cairo_1.18.4.orig.tar.xz' + Sha256 = '445ed8208a6e4823de1226a74ca319d3600e83f6369f99b14265006599c32ccb' + }, + @{ + Name = 'hicolor-icon-theme-0.18.tar.xz' + Url = 'http://deb.debian.org/debian/pool/main/h/hicolor-icon-theme/hicolor-icon-theme_0.18.orig.tar.xz' + Sha256 = 'db0e50a80aa3bf64bb45cbca5cf9f75efd9348cf2ac690b907435238c3cf81d7' + } +) + +$prefix = Join-Path $BuildRoot "gtk\$Platform\release" +$srcRoot = Join-Path $BuildRoot 'ci-src' + +function Write-Step ([string] $Name) { + Write-Host '' + Write-Host "=== $Name ===" -ForegroundColor Cyan +} + +function Invoke-Checked ([string] $What, [scriptblock] $Body) { + & $Body + if ($LASTEXITCODE -ne 0) { + throw "$What failed with exit code $LASTEXITCODE" + } +} + +function Get-SourceTree ([string] $Url, [string] $Name) { + New-Item -ItemType Directory -Force -Path $srcRoot | Out-Null + $zip = Join-Path $srcRoot "$Name.zip" + $out = Join-Path $srcRoot $Name + + if (-not (Test-Path $zip)) { + Write-Host "downloading $Url" + Invoke-WebRequest -Uri $Url -OutFile $zip -UseBasicParsing + } + if (-not (Test-Path $out)) { + Expand-Archive -Path $zip -DestinationPath $out -Force + } + + # GitHub source zips nest everything one directory deep. + $children = @(Get-ChildItem -Path $out -Directory) + if ($children.Count -eq 1) { return $children[0].FullName } + return $out +} + +Write-Step 'gvsbuild: GTK4 and friends' +$package = if ($GvsbuildVersion) { "gvsbuild==$GvsbuildVersion" } else { 'gvsbuild' } +Invoke-Checked "pip install $package" { python -m pip install --upgrade --disable-pip-version-check $package } +# --configuration release matters: gvsbuild defaults to debug-optimized, which +# would land the prefix in ...\gtk\x64\debug-optimized instead of ...\release. +$gvsSrc = Join-Path $BuildRoot 'src' +New-Item -ItemType Directory -Force -Path $gvsSrc | Out-Null +foreach ($archive in $SeededArchives) { + $dest = Join-Path $gvsSrc $archive.Name + if (Test-Path $dest) { + Write-Host "$($archive.Name) already seeded" + continue + } + Write-Host "seeding $($archive.Name) from $($archive.Url)" + Invoke-WebRequest -Uri $archive.Url -OutFile $dest -UseBasicParsing + $digest = (Get-FileHash -Path $dest -Algorithm SHA256).Hash + if ($digest -ne $archive.Sha256.ToUpper()) { + Remove-Item $dest -Force + throw "$($archive.Name) from $($archive.Url) hashed $digest, expected $($archive.Sha256)" + } +} + +# The remaining hosts are healthy but numerous, and a build this long shouldn't +# die on one of them blinking. --fast-build lets a retry resume rather than +# start the whole stack again; fetched archives are kept either way. +# --fast-build from the outset: it skips any project whose .wingtk-built marker +# is already in the build tree, so a restored cache resumes the stack instead of +# recompiling it. Its documented caveat -- stale results if the patches or the +# build script change underneath it -- is covered by pinning $GvsbuildVersion. +$gvsArgs = @( + 'build', + '--build-dir', $BuildRoot, + '--platform', $Platform, + '--configuration', 'release', + '--fast-build' +) +$attempts = 3 +for ($attempt = 1; $attempt -le $attempts; $attempt++) { + $attemptArgs = @($gvsArgs) + $attemptArgs += $GvsbuildProjects + + & gvsbuild @attemptArgs + if ($LASTEXITCODE -eq 0) { break } + if ($attempt -eq $attempts) { + throw "gvsbuild build failed with exit code $LASTEXITCODE after $attempts attempts" + } + Write-Warning "gvsbuild attempt $attempt failed with exit code $LASTEXITCODE; retrying" + Start-Sleep -Seconds 30 +} + +# gvsbuild builds libcurl with CMake, which names the import library +# libcurl_imp.lib -- gvsbuild patches libcurl.pc for exactly this reason. +# DepLibs in poxchat.props asks for libcurl.lib, the name curl's own Windows +# builds use, so give it that name; same fixup as websockets_static.lib below. +$curlImp = Join-Path $prefix 'lib\libcurl_imp.lib' +$curlLib = Join-Path $prefix 'lib\libcurl.lib' +if (-not (Test-Path $curlLib)) { + if (-not (Test-Path $curlImp)) { + throw "neither libcurl.lib nor libcurl_imp.lib in $prefix\lib" + } + Copy-Item $curlImp $curlLib -Force +} + +Write-Step 'jansson (static)' +$janssonSrc = Get-SourceTree "https://github.com/akheron/jansson/archive/refs/tags/$JanssonTag.zip" "jansson-$JanssonTag" +$janssonBuild = Join-Path $janssonSrc 'build-ci' +$janssonPrefix = Join-Path $BuildRoot 'jansson' +Invoke-Checked 'jansson configure' { + cmake -S $janssonSrc -B $janssonBuild -G $CMakeGenerator -A $Platform ` + -DCMAKE_INSTALL_PREFIX="$janssonPrefix" ` + -DJANSSON_BUILD_SHARED_LIBS=OFF ` + -DJANSSON_BUILD_DOCS=OFF ` + -DJANSSON_EXAMPLES=OFF ` + -DJANSSON_WITHOUT_TESTS=ON +} +Invoke-Checked 'jansson build' { cmake --build $janssonBuild --config Release --target install } + +Write-Step 'libwebsockets (static, against gvsbuild OpenSSL)' +$lwsSrc = Get-SourceTree "https://github.com/warmcat/libwebsockets/archive/refs/tags/$LibWebSocketsTag.zip" "libwebsockets-$LibWebSocketsTag" +$lwsBuild = Join-Path $lwsSrc 'build-ci' +$lwsPrefix = Join-Path $BuildRoot 'libwebsockets' +Invoke-Checked 'libwebsockets configure' { + cmake -S $lwsSrc -B $lwsBuild -G $CMakeGenerator -A $Platform ` + -DCMAKE_INSTALL_PREFIX="$lwsPrefix" ` + -DCMAKE_PREFIX_PATH="$prefix" ` + -DLWS_WITH_STATIC=ON ` + -DLWS_WITH_SHARED=OFF ` + -DLWS_WITH_SSL=ON ` + -DLWS_OPENSSL_INCLUDE_DIRS="$prefix\include" ` + -DLWS_OPENSSL_LIBRARIES="$prefix\lib\libssl.lib;$prefix\lib\libcrypto.lib" ` + -DLWS_WITHOUT_TESTAPPS=ON ` + -DLWS_WITHOUT_TEST_SERVER=ON ` + -DLWS_WITHOUT_TEST_SERVER_EXTPOLL=ON ` + -DLWS_WITHOUT_TEST_PING=ON ` + -DLWS_WITHOUT_TEST_CLIENT=ON ` + -DLWS_WITH_MINIMAL_EXAMPLES=OFF +} +Invoke-Checked 'libwebsockets build' { cmake --build $lwsBuild --config Release --target install } + +# The static build installs websockets_static.lib; DepLibs asks for +# websockets.lib, which is what a shared build would have produced. +$lwsStatic = Join-Path $lwsPrefix 'lib\websockets_static.lib' +if (Test-Path $lwsStatic) { + Copy-Item $lwsStatic (Join-Path $lwsPrefix 'lib\websockets.lib') -Force +} + +Write-Step 'WinSparkle' +$wsPrefix = Join-Path $BuildRoot "WinSparkle\$Platform" +$wsSrc = Get-SourceTree "https://github.com/vslavik/winsparkle/releases/download/v$WinSparkleVersion/WinSparkle-$WinSparkleVersion.zip" "WinSparkle-$WinSparkleVersion" +New-Item -ItemType Directory -Force -Path $wsPrefix | Out-Null + +# The release zip carries every architecture; pick ours by path rather than by +# guessing at the layout, which has moved between releases. +$archPattern = if ($Platform -eq 'x64') { '(?i)(x64|amd64)' } else { '(?i)win32|x86' } +foreach ($file in 'WinSparkle.dll', 'WinSparkle.lib') { + $found = Get-ChildItem -Path $wsSrc -Recurse -Filter $file | + Where-Object { $_.FullName -match $archPattern } | + Select-Object -First 1 + if (-not $found) { + throw "no $Platform $file in the WinSparkle $WinSparkleVersion zip" + } + Copy-Item $found.FullName $wsPrefix -Force +} +# winsparkle.h includes winsparkle-version.h, so take the whole include +# directory rather than naming the headers -- upd.vcxproj compiles against +# whatever this release ships, not a list written here. +$header = Get-ChildItem -Path $wsSrc -Recurse -Filter 'winsparkle.h' | Select-Object -First 1 +if (-not $header) { + throw "no winsparkle.h in the WinSparkle $WinSparkleVersion zip" +} +Copy-Item (Join-Path $header.Directory.FullName '*.h') $wsPrefix -Force + +$copying = Get-ChildItem -Path $wsSrc -Recurse -Filter 'COPYING' | Select-Object -First 1 +if (-not $copying) { + throw "no COPYING in the WinSparkle $WinSparkleVersion zip" +} +Copy-Item $copying.FullName $wsPrefix -Force + +Write-Step 'CA bundle' +$certDir = Join-Path $BuildRoot 'cert' +New-Item -ItemType Directory -Force -Path $certDir | Out-Null +Invoke-WebRequest -Uri 'https://curl.se/ca/cacert.pem' -OutFile (Join-Path $certDir 'cacert.pem') -UseBasicParsing + +Write-Step 'done' +Write-Host "gvsbuild prefix: $prefix" +Write-Host "jansson: $janssonPrefix" +Write-Host "libwebsockets: $lwsPrefix" +Write-Host "WinSparkle: $wsPrefix" diff --git a/win32/ci/check-imports.ps1 b/win32/ci/check-imports.ps1 new file mode 100644 index 00000000..4dc02edd --- /dev/null +++ b/win32/ci/check-imports.ps1 @@ -0,0 +1,221 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Name every DLL the staged tree imports but does not carry. + +.DESCRIPTION + Windows has no ldd, and a staged tree missing one dependency reports it as + STATUS_DLL_NOT_FOUND (0xC0000135) the moment the process starts, with no + name attached. The smoke test catches that a build is broken; it cannot say + what is missing, so finding the set costs one runner cycle per file. + + This reads the import and delay-import tables out of the PE headers itself + -- no dumpbin, no VC environment -- and reports everything unresolved at + once, saying where in -Prefix each missing file can be found. + + Resolution follows the loader for the process directory: an import is + satisfied if it sits beside poxchat.exe in -Root, is an API set, or is a + system DLL. DLLs in subdirectories of the tree (plugins, lib\enchant-2) + are scanned too -- their imports resolve against the process directory, not + their own -- so they are checked the same way. +#> + +[CmdletBinding()] +param ( + # The staged tree, i.e. the directory poxchat.exe runs from. + [Parameter(Mandatory = $true)] + [string] $Root, + + # Directories to search when reporting where a missing DLL could come from. + [string[]] $Prefix = @() +) + +$ErrorActionPreference = 'Stop' + +# --- PE reading ------------------------------------------------------------- + +function Get-PeString ($bytes, [int] $offset) +{ + if ($offset -lt 0 -or $offset -ge $bytes.Length) { return $null } + $end = $offset + while ($end -lt $bytes.Length -and $bytes[$end] -ne 0) { $end++ } + return [System.Text.Encoding]::ASCII.GetString($bytes, $offset, $end - $offset) +} + +function Get-PeImports ([string] $Path) +{ + $bytes = [System.IO.File]::ReadAllBytes($Path) + if ($bytes.Length -lt 0x40) { return @() } + if ($bytes[0] -ne 0x4D -or $bytes[1] -ne 0x5A) { return @() } # MZ + + $pe = [BitConverter]::ToInt32($bytes, 0x3C) + if ($pe -le 0 -or $pe + 24 -gt $bytes.Length) { return @() } + if ([BitConverter]::ToUInt32($bytes, $pe) -ne 0x00004550) { return @() } # PE\0\0 + + $sectionCount = [BitConverter]::ToUInt16($bytes, $pe + 6) + $optSize = [BitConverter]::ToUInt16($bytes, $pe + 20) + $opt = $pe + 24 + $magic = [BitConverter]::ToUInt16($bytes, $opt) + + # PE32+ widens ImageBase to 8 bytes and pushes the data directory along + # with it. + if ($magic -eq 0x20B) { + $dirs = $opt + 112 + } elseif ($magic -eq 0x10B) { + $dirs = $opt + 96 + } else { + return @() + } + $dirCount = [BitConverter]::ToUInt32($bytes, $dirs - 4) + + # The section table follows the optional header and is what turns an RVA + # into a file offset. + $sections = @() + for ($i = 0; $i -lt $sectionCount; $i++) { + $at = $opt + $optSize + $i * 40 + if ($at + 40 -gt $bytes.Length) { break } + $virtual = [BitConverter]::ToUInt32($bytes, $at + 8) + $raw = [BitConverter]::ToUInt32($bytes, $at + 16) + $sections += [pscustomobject]@{ + Rva = [BitConverter]::ToUInt32($bytes, $at + 12) + Span = [Math]::Max($virtual, $raw) + File = [BitConverter]::ToUInt32($bytes, $at + 20) + } + } + + $toOffset = { + param([uint32] $rva) + foreach ($sec in $sections) { + if ($rva -ge $sec.Rva -and $rva -lt $sec.Rva + $sec.Span) { + return [int]($sec.File + ($rva - $sec.Rva)) + } + } + return -1 + } + + $names = @() + + # Import directory: 20-byte descriptors, DLL name RVA at +12, terminated by + # an all-zero descriptor. + if ($dirCount -gt 1) { + $rva = [BitConverter]::ToUInt32($bytes, $dirs + 8) + if ($rva -ne 0) { + $at = & $toOffset $rva + while ($at -ge 0 -and $at + 20 -le $bytes.Length) { + $nameRva = [BitConverter]::ToUInt32($bytes, $at + 12) + if ($nameRva -eq 0) { break } + $name = Get-PeString $bytes (& $toOffset $nameRva) + if ($name) { $names += $name } + $at += 20 + } + } + } + + # Delay-import directory: 32-byte descriptors, name at +4. Attribute bit 0 + # says the fields are RVAs; the original 1990s format stored virtual + # addresses instead, and nothing that can build this tree emits it, so that + # form is left alone rather than guessed at. + if ($dirCount -gt 13) { + $rva = [BitConverter]::ToUInt32($bytes, $dirs + 13 * 8) + if ($rva -ne 0) { + $at = & $toOffset $rva + while ($at -ge 0 -and $at + 32 -le $bytes.Length) { + $attrs = [BitConverter]::ToUInt32($bytes, $at) + $nameRva = [BitConverter]::ToUInt32($bytes, $at + 4) + if ($nameRva -eq 0) { break } + if (-not ($attrs -band 1)) { + Write-Warning "$Path uses address-based delay imports; not walking them" + break + } + $name = Get-PeString $bytes (& $toOffset $nameRva) + if ($name) { $names += $name } + $at += 32 + } + } + } + + return $names +} + +# --- what counts as resolved ------------------------------------------------ + +$Root = (Resolve-Path -LiteralPath $Root).Path + +$beside = @{} +foreach ($file in Get-ChildItem -LiteralPath $Root -File) { + $beside[$file.Name.ToLowerInvariant()] = $true +} + +# System32 covers the Windows SDK import libraries the tree links against. +# Not filtered to *.dll: gtk-4-1.dll binds to winspool.drv, and .cpl and .ocx +# are equally bindable. api-ms-win-* / ext-ms-win-* are handled separately +# below, since the loader resolves those from a schema rather than from files. +$system = @{} +foreach ($dir in @("$env:SystemRoot\System32", "$env:SystemRoot\SysWOW64")) { + if (-not (Test-Path -LiteralPath $dir)) { continue } + foreach ($file in Get-ChildItem -LiteralPath $dir -File -ErrorAction SilentlyContinue) { + $system[$file.Name.ToLowerInvariant()] = $true + } +} + +# Where a missing DLL could be picked up from, for the report. +$available = @{} +foreach ($dir in $Prefix) { + if (-not (Test-Path -LiteralPath $dir)) { continue } + foreach ($file in Get-ChildItem -LiteralPath $dir -File -ErrorAction SilentlyContinue) { + $key = $file.Name.ToLowerInvariant() + if (-not $available.ContainsKey($key)) { $available[$key] = $file.FullName } + } +} + +# --- walk ------------------------------------------------------------------- + +$binaries = Get-ChildItem -LiteralPath $Root -File -Recurse | + Where-Object { $_.Extension -in '.exe', '.dll', '.pyd' } + +# A missing DLL has dependencies of its own, and staging it only to find out +# next run what it drags in is the same one-cycle-per-file trap. So when one +# turns up in the prefix, walk it too: the report names the whole chain. +$queue = [System.Collections.Generic.Queue[object]]::new() +foreach ($binary in $binaries) { + $queue.Enqueue([pscustomobject]@{ + Path = $binary.FullName + Label = $binary.FullName.Substring($Root.Length).TrimStart('\') + }) +} + +$missing = @{} +$followed = @{} +while ($queue.Count -gt 0) { + $item = $queue.Dequeue() + foreach ($import in Get-PeImports $item.Path) { + $key = $import.ToLowerInvariant() + if ($beside.ContainsKey($key)) { continue } + if ($system.ContainsKey($key)) { continue } + if ($key -match '^(api|ext)-ms-win-') { continue } + if (-not $missing.ContainsKey($key)) { $missing[$key] = @() } + $missing[$key] += $item.Label + if ($available.ContainsKey($key) -and -not $followed.ContainsKey($key)) { + $followed[$key] = $true + $queue.Enqueue([pscustomobject]@{ Path = $available[$key]; Label = $import }) + } + } +} + +Write-Host "scanned $($binaries.Count) binaries under $Root" + +if ($missing.Count -eq 0) { + Write-Host 'every import resolves inside the staged tree' + exit 0 +} + +Write-Host '' +Write-Host "$($missing.Count) unresolved:" +foreach ($key in ($missing.Keys | Sort-Object)) { + $from = if ($available.ContainsKey($key)) { $available[$key] } else { 'NOT IN THE DEPENDENCY PREFIX EITHER' } + Write-Host " $key" + Write-Host " wanted by: $(($missing[$key] | Sort-Object -Unique) -join ', ')" + Write-Host " found at: $from" +} + +throw "$($missing.Count) DLL(s) missing from the staged tree" diff --git a/win32/ci/make-installer.ps1 b/win32/ci/make-installer.ps1 new file mode 100644 index 00000000..0a50f7e3 --- /dev/null +++ b/win32/ci/make-installer.ps1 @@ -0,0 +1,83 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Generate poxchat.iss from its template and compile it with Inno Setup. + +.DESCRIPTION + win32\installer\installer.vcxproj does this as a pre-build event; doing it + here instead keeps the quoting sane and lets CI build the installer as a + separate, skippable step. + + poxchat.iss.tt targets Inno Setup 5 and #includes idp.iss from the Inno + Download Plugin (which fetches vcredist/perl/python at install time). The + hosted runners ship Inno Setup 6 and no idp, so -Provision installs both: + IS5 from jrsoftware, idp from the mirror hexchat kept on its gvsbuild + releases, the upstream host having gone away years ago. + + The compiled installer lands next to the staged tree, in \x64\, + per the iss file's own OutputDir. + +.NOTES + Inno Setup's /d switch cannot take a quoted value ending in a backslash, so + -RepoRoot must not contain spaces. +#> + +[CmdletBinding()] +param ( + [string] $RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path, + [Parameter(Mandatory = $true)] + [string] $BuildDir, + [string] $Platform = 'x64', + [switch] $Provision, + [string] $InnoSetupUrl = 'https://files.jrsoftware.org/is/5/innosetup-5.6.1-unicode.exe', + [string] $IdpUrl = 'https://github.com/hexchat/gvsbuild/releases/download/hexchat-2.16.2/idpsetup-1.5.1.exe' +) + +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' + +$iscc = Join-Path ${env:ProgramFiles(x86)} 'Inno Setup 5\ISCC.exe' + +if ($Provision -and -not (Test-Path $iscc)) { + $tmpBase = if ($env:RUNNER_TEMP) { $env:RUNNER_TEMP } else { $env:TEMP } + $tmp = Join-Path $tmpBase 'inno' + New-Item -ItemType Directory -Force -Path $tmp | Out-Null + + Write-Host "installing Inno Setup 5 from $InnoSetupUrl" + $setup = Join-Path $tmp 'innosetup.exe' + Invoke-WebRequest -Uri $InnoSetupUrl -OutFile $setup -UseBasicParsing + Start-Process -FilePath $setup -ArgumentList '/VERYSILENT', '/SUPPRESSMSGBOXES', '/NORESTART' -Wait + + Write-Host "installing Inno Download Plugin from $IdpUrl" + $idp = Join-Path $tmp 'idpsetup.exe' + Invoke-WebRequest -Uri $IdpUrl -OutFile $idp -UseBasicParsing + Start-Process -FilePath $idp -ArgumentList '/VERYSILENT', '/SUPPRESSMSGBOXES', '/NORESTART' -Wait +} + +if (-not (Test-Path $iscc)) { + throw "Inno Setup 5 not found at $iscc (re-run with -Provision to install it)" +} + +$binDir = Join-Path $BuildDir "$Platform\bin" +$relDir = Join-Path $BuildDir "$Platform\rel" +if (-not (Test-Path $relDir)) { + throw "no staged tree at $relDir -- build the 'copy' project first" +} + +# version-template.ps1 reads the version out of ${SOLUTIONDIR}meson.build, so +# SOLUTIONDIR is the repo root *with* a trailing separator. +$env:SOLUTIONDIR = $RepoRoot.TrimEnd('\') + '\' +$template = Join-Path $RepoRoot 'win32\installer\poxchat.iss.tt' +$iss = Join-Path $binDir 'poxchat.iss' +New-Item -ItemType Directory -Force -Path $binDir | Out-Null +& (Join-Path $RepoRoot 'win32\version-template.ps1') $template $iss + +$projectDir = (Join-Path $RepoRoot 'win32\installer').TrimEnd('\') + '\' +& $iscc /dPROJECTDIR=$projectDir /dAPPARCH=$Platform $iss +if ($LASTEXITCODE -ne 0) { + throw "ISCC failed with exit code $LASTEXITCODE" +} + +Get-ChildItem -Path (Join-Path $BuildDir $Platform) -Filter '*.exe' | ForEach-Object { + Write-Host "built $($_.FullName)" +} diff --git a/win32/copy/copy.vcxproj b/win32/copy/copy.vcxproj index e056ebb0..24a04056 100644 --- a/win32/copy/copy.vcxproj +++ b/win32/copy/copy.vcxproj @@ -51,6 +51,9 @@ + + @@ -76,7 +79,6 @@ - @@ -101,9 +103,17 @@ + + + + + @@ -120,7 +130,6 @@ - diff --git a/win32/installer/poxchat.iss.tt b/win32/installer/poxchat.iss.tt index 32a32730..f07a958b 100644 --- a/win32/installer/poxchat.iss.tt +++ b/win32/installer/poxchat.iss.tt @@ -113,7 +113,6 @@ Source: "changelog.url"; DestDir: "{app}"; Flags: ignoreversion; Components: lib Source: "readme.url"; DestDir: "{app}"; Flags: ignoreversion; Components: libs Source: "share\xml\*"; DestDir: "{app}\share\xml"; Flags: ignoreversion createallsubdirs recursesubdirs; Components: libs Source: "share\doc\*"; DestDir: "{app}\share\doc"; Flags: ignoreversion createallsubdirs recursesubdirs; Components: libs -Source: "share\themes\MS-Windows\*"; DestDir: "{app}\share\themes\MS-Windows"; Flags: ignoreversion createallsubdirs recursesubdirs; Components: libs Source: "share\locale\*"; DestDir: "{app}\share\locale"; Flags: ignoreversion createallsubdirs recursesubdirs; Components: translations Source: "etc\fonts\*"; DestDir: "{app}\etc\fonts"; Flags: ignoreversion createallsubdirs recursesubdirs; Components: libs @@ -161,6 +160,7 @@ Source: "plugins\hcnotifications-winrt.dll"; DestDir: "{app}\plugins"; Flags: ig ; Enchant spell checking (optional - may not be installed) Source: "libenchant-2.dll"; DestDir: "{app}"; Flags: ignoreversion skipifsourcedoesntexist; Components: libs +Source: "libenchant.dll"; DestDir: "{app}"; Flags: ignoreversion skipifsourcedoesntexist; Components: libs Source: "lib\enchant-2\*"; DestDir: "{app}\lib\enchant-2"; Flags: ignoreversion skipifsourcedoesntexist; Components: libs ; GTK4 uses CSS theming only - no theme engine DLLs needed diff --git a/win32/poxchat.props b/win32/poxchat.props index 30fc0330..f1d87549 100644 --- a/win32/poxchat.props +++ b/win32/poxchat.props @@ -54,7 +54,10 @@ $(DepsRoot)\include\gtk-4.0;$(DepsRoot)\lib\gtk-4.0\include;$(DepsRoot)\include\cairo;$(DepsRoot)\include\harfbuzz;$(DepsRoot)\include\pango-1.0;$(DepsRoot)\include\gdk-pixbuf-2.0;$(DepsRoot)\include\graphene-1.0;$(DepsRoot)\lib\graphene-1.0\include - gtk-4.lib;glib-2.0.lib;gio-2.0.lib;gdk_pixbuf-2.0.lib;pangowin32-1.0.lib;pangocairo-1.0.lib;pango-1.0.lib;cairo.lib;gobject-2.0.lib;gmodule-2.0.lib;glib-2.0.lib;intl.lib;xml2.lib;libcrypto.lib;libssl.lib;graphene-1.0.lib;jansson.lib;websockets.lib;libcurl.lib;sqlite3.lib;wininet.lib;winmm.lib;ws2_32.lib + + gtk-4.lib;glib-2.0.lib;gio-2.0.lib;gdk_pixbuf-2.0.lib;pangowin32-1.0.lib;pangocairo-1.0.lib;pango-1.0.lib;cairo.lib;gobject-2.0.lib;gmodule-2.0.lib;glib-2.0.lib;intl.lib;xml2.lib;libcrypto.lib;libssl.lib;graphene-1.0.lib;jansson.lib;websockets.lib;libcurl.lib;sqlite3.lib;wininet.lib;winmm.lib;ws2_32.lib;crypt32.lib $(SolutionDir)..\data\\ $(SolutionDir)..\..\poxchat-build-gtk4 $(PoxChatBuild)\$(PlatformName)\bin\ @@ -103,8 +106,12 @@ true true UseLinkTimeCodeGeneration - - $(OpenSSLLib);%(AdditionalLibraryDirectories) + + $(OpenSSLLib);$(JanssonLib);$(LibWebSocketsLib);$(LibCurlLib);%(AdditionalLibraryDirectories)