XP Release. #7
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
| name: XP Release. | |
| # Release build of the Windows XP port, for both of its targets: | |
| # | |
| # x86 Windows XP SP3. Subsystem 5.01. | |
| # x64 Windows XP Professional x64 Edition, which is NT 5.2 - the Server 2003 | |
| # kernel - so its images declare subsystem 5.02 and a 5.01 link is | |
| # rejected outright. It is a separate download and a separate feed key | |
| # (winxp64 / txp64upd), because the x86 build ALSO runs there under | |
| # WOW64 and offering one to the other would silently move a machine onto | |
| # the 32-bit line for good. | |
| # | |
| # Everything below runs once per architecture; the `arch` dispatch input cuts it | |
| # down to one while iterating. The two share only the runner image, the patched | |
| # SDK 7.1A headers and the Qt source checkout - toolchain objects, libraries, Qt | |
| # prefix and build tree are all per-architecture, in sibling directories. | |
| # | |
| # Two jobs with very different requirements: | |
| # | |
| # patches runs anywhere. It proves the repository is self-consistent: every | |
| # patches/*.patch still applies to the submodule commit the tree pins. | |
| # That is the failure this port actually suffers - a regenerated patch | |
| # silently losing a hunk, or an upstream bump moving the code a patch | |
| # targets - and it needs no toolchain, so it gates every push. | |
| # | |
| # build needs the XP toolchain, which GitHub-hosted runners cannot provide: | |
| # the v141_xp 14.16 target, a PATCHED SDK 7.1A include tree, the | |
| # fpcompat helper objects and the XP-pinned prebuilt dependencies | |
| # (Qt 5.15.16 static, OpenSSL 1.0.2, FFmpeg 3.4, OpenAL, opus). None of | |
| # that is installable from a hosted image, so this job targets a | |
| # self-hosted runner that already carries them. See xp/README.md. | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| runner_labels: | |
| description: 'JSON array of runner labels carrying the XP toolchain.' | |
| type: string | |
| default: '["self-hosted","windows","xp-toolchain"]' | |
| arch: | |
| description: 'Which target to build. x86 is XP SP3, x64 is XP Professional x64 Edition (NT 5.2).' | |
| type: choice | |
| options: [both, x86, x64] | |
| default: both | |
| rebuild_ffmpeg: | |
| description: 'Rebuild the XP FFmpeg libraries first (~10 minutes).' | |
| type: boolean | |
| default: false | |
| only: | |
| description: 'Run just one stage while iterating (libraries, qt, build, publish) or all.' | |
| type: choice | |
| options: [all, libraries, qt, build, publish] | |
| default: all | |
| publish_from_run: | |
| description: 'Publish the package built by this run id instead of building one.' | |
| type: string | |
| default: '' | |
| publish_release: | |
| description: 'Publish a GitHub Release with the portable archive.' | |
| type: boolean | |
| default: false | |
| publish_update: | |
| description: 'Sign the update package and publish it to the Telegram feed.' | |
| type: choice | |
| # Quoted: bare `off` is a YAML boolean, so the choice list would offer | |
| # `false` and a dispatch with 'off' would be rejected. | |
| # | |
| # `package` signs the update and uploads it as an artifact without | |
| # posting anything. It exists because packing was otherwise reachable | |
| # only together with publishing, so the binary a machine would actually | |
| # install could not be tried out before it went live. Build with | |
| # `package`, install that artifact somewhere, then republish it with | |
| # `only=publish` + `publish_from_run` - a minute instead of a rebuild. | |
| options: ['off', package, rehearsal, testing, released] | |
| default: 'off' | |
| push: | |
| tags: | |
| - 'xp-v*' | |
| pull_request: | |
| paths: | |
| - 'patches/**' | |
| - 'xp/**' | |
| - '.gitmodules' | |
| - '.github/workflows/xp_release.yml' | |
| permissions: | |
| contents: write | |
| concurrency: | |
| # Per RUN, not per ref: the Qt stage takes hours and would otherwise queue | |
| # every other dispatch behind it. | |
| group: xp-release-${{ github.run_id }} | |
| cancel-in-progress: false | |
| jobs: | |
| patches: | |
| name: Patch set. | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| outputs: | |
| port: ${{ steps.detect.outputs.port }} | |
| arches: ${{ steps.detect.outputs.arches }} | |
| steps: | |
| - name: Clone. | |
| uses: actions/checkout@v7 | |
| with: | |
| submodules: recursive | |
| # GitHub only lets workflow_dispatch resolve a workflow that exists on the | |
| # DEFAULT branch, while the run itself uses the file from the ref you pick. | |
| # So a copy of this file has to live on a branch that carries no XP port at | |
| # all - and running it there must not fail. Detect and stand down instead. | |
| - name: Detect the port. | |
| id: detect | |
| env: | |
| ARCH: ${{ github.event.inputs.arch }} | |
| run: | | |
| if [ -f patches/apply_xp_patches.cmake ] && [ -d xp ]; then | |
| echo "port=true" >> $GITHUB_OUTPUT | |
| echo "XP port present, checking it." | |
| else | |
| echo "port=false" >> $GITHUB_OUTPUT | |
| echo "::notice::this ref carries no XP port - dispatch this workflow against an xp-port-* branch" | |
| fi | |
| # The build matrix, decided once here rather than in three copies of a | |
| # nested ternary. Empty on a push or a pull_request, which is `both`. | |
| case "$ARCH" in | |
| x86) arches='["x86"]' ;; | |
| x64) arches='["x64"]' ;; | |
| *) arches='["x86","x64"]' ;; | |
| esac | |
| echo "arches=$arches" >> $GITHUB_OUTPUT | |
| echo "building: $arches" | |
| # git hands .patch files out with CRLF on a client that translates line | |
| # endings, and then git apply rejects its own context lines while the base | |
| # blobs match perfectly. patches/.gitattributes marks them -text; this | |
| # verifies the result rather than trusting it. | |
| - name: Line endings. | |
| if: steps.detect.outputs.port == 'true' | |
| run: | | |
| bad=0 | |
| for patch in patches/*.patch; do | |
| if grep -qU $'\r' "$patch"; then | |
| echo "::error file=$patch::contains CR - patches/.gitattributes should keep it -text" | |
| bad=1 | |
| fi | |
| done | |
| if [ "$bad" = "0" ]; then echo "all patches are LF-only."; fi | |
| exit $bad | |
| # The authoritative list lives in apply_xp_patches.cmake, so read it from | |
| # there instead of repeating it here - a patch added without a CI update | |
| # would otherwise go unchecked. | |
| - name: Every patch applies to its pinned submodule. | |
| if: steps.detect.outputs.port == 'true' | |
| run: | | |
| set -e | |
| list=$(sed -n '/set(xp_patch_list/,/^)/p' patches/apply_xp_patches.cmake \ | |
| | grep -oE '"[^"]+"' | tr -d '"' | paste - -) | |
| [ -n "$list" ] || { echo "::error::could not read xp_patch_list"; exit 1; } | |
| failed=0 | |
| while IFS=$'\t' read -r dir file; do | |
| [ -n "$dir" ] || continue | |
| if [ ! -e "$dir/.git" ]; then | |
| echo "::error::submodule $dir is not checked out" | |
| failed=1 | |
| continue | |
| fi | |
| # A pristine checkout must take the patch cleanly. --check does not | |
| # touch the tree, so the order of the loop does not matter. | |
| if git -C "$dir" apply --check --binary "$GITHUB_WORKSPACE/patches/$file"; then | |
| echo "ok $file -> $dir" | |
| else | |
| echo "::error file=patches/$file::does not apply to $dir at its pinned commit" | |
| failed=1 | |
| fi | |
| done <<< "$list" | |
| exit $failed | |
| - name: The build recipe is present. | |
| if: steps.detect.outputs.port == 'true' | |
| run: | | |
| set -e | |
| for required in \ | |
| xp/build_ffmpeg_xp.sh \ | |
| xp/cmake_xp.ps1 \ | |
| xp/xpsafe.ps1 \ | |
| xp/bootstrap_toolchain.ps1 \ | |
| xp/build_libraries.ps1 \ | |
| xp/build_qt.ps1 \ | |
| xp/xp_env.ps1 \ | |
| xp/fpcompat/xpfls.asm \ | |
| xp/fpcompat/xpfls64.asm \ | |
| xp/fpcompat/hoststub.c \ | |
| xp/README.md; do | |
| [ -f "$required" ] || { echo "::error::$required is missing"; exit 1; } | |
| done | |
| echo "recipe complete." | |
| libraries: | |
| name: XP toolchain and libraries (${{ matrix.arch }}). | |
| needs: patches | |
| runs-on: windows-latest | |
| timeout-minutes: 300 | |
| if: github.event_name != 'pull_request' && needs.patches.outputs.port == 'true' | |
| && (github.event.inputs.only == null || github.event.inputs.only == 'all' || github.event.inputs.only == 'libraries') | |
| strategy: | |
| # The two architectures share nothing but the runner image, so one failing | |
| # must not cancel the other - and a partial cache is still worth saving. | |
| fail-fast: false | |
| matrix: | |
| arch: ${{ fromJSON(needs.patches.outputs.arches) }} | |
| env: | |
| XP_TOOLCHAIN_ROOT: C:\xp-toolchain | |
| # Set once per job: every script in xp/ takes its default architecture | |
| # from here, so nothing downstream has to be told again. | |
| XP_ARCH: ${{ matrix.arch }} | |
| # Separate trees, not a switchable one. Each library writes into its own | |
| # source checkout, so a 64-bit zlibstat.lib landing where CMake expects | |
| # the 32-bit one would only show up as a link error hours later. | |
| XP_LIBS_DIR_WIN: ${{ matrix.arch == 'x64' && 'C:\xp-toolchain\Libraries-x64' || 'C:\xp-toolchain\Libraries' }} | |
| defaults: | |
| run: | |
| shell: pwsh | |
| steps: | |
| - name: Clone. | |
| uses: actions/checkout@v7 | |
| # Everything the port needs that a hosted image lacks, built from | |
| # Microsoft's own installers plus this repository's sources: the v141 | |
| # (14.16) target toolset, the 7.1A SDK through the VS2019 Build Tools | |
| # bootstrapper, the patched include tree and fpcompat. | |
| - name: Bootstrap the XP toolchain. | |
| run: | | |
| & "$PWD\xp\bootstrap_toolchain.ps1" -Root C:\xp-toolchain -Arch ${{ matrix.arch }} | |
| if ($LASTEXITCODE -ne 0) { exit 1 } | |
| # Prove the toolchain before spending hours on libraries: build a minimal | |
| # program exactly the way Telegram is built and let xpsafe.ps1 confirm it | |
| # could start on Windows XP. | |
| - name: Compile a minimal XP binary. | |
| run: | | |
| $repoRoot = $PWD.Path | |
| $probe = Join-Path $env:RUNNER_TEMP 'xpprobe' | |
| New-Item -ItemType Directory -Force -Path $probe | Out-Null | |
| @' | |
| #include <windows.h> | |
| #include <stdio.h> | |
| #include <mutex> | |
| #include <vector> | |
| int main() { | |
| std::mutex m; | |
| std::lock_guard<std::mutex> guard(m); | |
| std::vector<double> v{ 1.5, 2.5 }; | |
| printf("xp probe %d %.1f\n", (int)v.size(), v[0] + v[1]); | |
| return 0; | |
| } | |
| '@ | Set-Content -Path "$probe\xpprobe.cpp" -Encoding ascii | |
| & "$repoRoot\xp\xp_env.ps1" -Toolchain C:\xp-toolchain | |
| $forced = Join-Path $repoRoot 'xp\xp-compat.h' | |
| Push-Location $probe | |
| & cl.exe /nologo /MT /EHsc /d2FH4- /FI"$forced" /c xpprobe.cpp 2>&1 | Write-Host | |
| if ($LASTEXITCODE -ne 0) { Write-Error 'compile failed'; exit 1 } | |
| & link.exe /nologo "/SUBSYSTEM:CONSOLE,$env:XP_SUBSYSTEM_VERSION" /FORCE:MULTIPLE xpprobe.obj ` | |
| "$env:XP_FPCOMPAT\fpcompat.lib" /OUT:xpprobe.exe 2>&1 | Write-Host | |
| Write-Host "linked for $env:XP_ARCH, subsystem $env:XP_SUBSYSTEM_VERSION" | |
| if ($LASTEXITCODE -ne 0) { Write-Error 'link failed'; exit 1 } | |
| Pop-Location | |
| & "$repoRoot\xp\xpsafe.ps1" -Exe "$probe\xpprobe.exe" -Dumpbin ( | |
| Get-ChildItem "$env:XP_TOOLSET_BINARY_DIR\bin\Hostx64\x64\dumpbin.exe").FullName | |
| if ($LASTEXITCODE -ne 0) { exit 1 } | |
| # The libraries are pinned by commit, so the key follows the recipe alone. | |
| # Restore and save are split: actions/cache saves nothing when the job | |
| # fails, and these builds make real progress worth banking either way. | |
| - name: Libraries cache. | |
| uses: actions/cache/restore@v6 | |
| with: | |
| path: ${{ env.XP_LIBS_DIR_WIN }} | |
| key: ${{ runner.os }}-xp-libs-${{ matrix.arch }}-${{ hashFiles('xp/build_libraries.ps1', 'xp/deps/**') }}-${{ github.run_id }} | |
| restore-keys: ${{ runner.os }}-xp-libs-${{ matrix.arch }}-${{ hashFiles('xp/build_libraries.ps1', 'xp/deps/**') }}- | |
| - name: Build the pinned libraries. | |
| run: | | |
| & "$PWD\xp\build_libraries.ps1" -Root $env:XP_LIBS_DIR_WIN -Toolchain C:\xp-toolchain -Arch ${{ matrix.arch }} | |
| if ($LASTEXITCODE -ne 0) { exit 1 } | |
| # FFmpeg's configure is a shell script and wants make, nasm and pkg-config, | |
| # so it runs under msys2 exactly as it does on the workstation. It has to | |
| # come after opus, which it links. | |
| - name: msys2. | |
| uses: msys2/setup-msys2@v2 | |
| with: | |
| msystem: MINGW64 | |
| update: false | |
| path-type: inherit | |
| install: >- | |
| make | |
| diffutils | |
| nasm | |
| pkgconf | |
| # No architecture flag: ffmpeg's configure decides the subarch by compiling | |
| # a _M_X64 probe with whichever cl.exe the environment provides, so it | |
| # follows the toolchain rather than being told. | |
| - name: FFmpeg. | |
| run: | | |
| $ff = Join-Path $env:XP_LIBS_DIR_WIN 'ffmpeg' | |
| if (Test-Path "$ff\libavcodec\libavcodec.a") { Write-Host 'already built'; exit 0 } | |
| if (-not (Test-Path "$ff\configure")) { | |
| & git.exe clone --depth 1 -b release/3.4 https://github.com/FFmpeg/FFmpeg.git $ff | |
| if ($LASTEXITCODE -ne 0) { Write-Error 'cloning FFmpeg failed'; exit 1 } | |
| } | |
| & "$PWD\xp\run_ffmpeg_recipe.ps1" -Root $env:XP_LIBS_DIR_WIN -Toolchain C:\xp-toolchain -Arch ${{ matrix.arch }} | |
| if ($LASTEXITCODE -ne 0) { exit 1 } | |
| - name: What got built. | |
| if: always() | |
| run: | | |
| # Two of these move with the architecture, and both are named by | |
| # cmake/external as well - zlib off build_win64, lzma off the platform | |
| # subdirectory a .vcxproj adds. If they drift apart, the link fails. | |
| $arch = $env:XP_ARCH | |
| $platformDir = $(if ($arch -eq 'x64') { 'x64\' } else { '' }) | |
| $expected = @( | |
| "zlib\contrib\vstudio\vc14\$arch\ZlibStatReleaseWithoutAsm\zlibstat.lib", | |
| 'zlib\contrib\minizip\Release\libminizips.lib', | |
| "lzma\C\Util\LzmaLib\${platformDir}Release\LzmaLib.lib", | |
| 'opus\out\Release\opus.lib', | |
| 'openssl\out32\libeay32.lib', | |
| 'openssl\out32\ssleay32.lib', | |
| 'openal-soft\build\Release\OpenAL32.lib', | |
| 'ada\out\singleheader\Release\ada-singleheader-lib.lib', | |
| 'ffmpeg\libavcodec\libavcodec.a', | |
| 'ffmpeg\libavformat\libavformat.a', | |
| 'ffmpeg\libavfilter\libavfilter.a') | |
| $missing = 0 | |
| foreach ($rel in $expected) { | |
| $full = Join-Path $env:XP_LIBS_DIR_WIN $rel | |
| if (Test-Path $full) { | |
| Write-Host (" ok {0,12:N0} {1}" -f (Get-Item $full).Length, $rel) | |
| } else { | |
| Write-Host " MISSING $rel" | |
| $missing++ | |
| } | |
| } | |
| if ($missing -gt 0) { Write-Error "$missing libraries missing"; exit 1 } | |
| - name: Save the libraries cache. | |
| if: always() | |
| uses: actions/cache/save@v6 | |
| with: | |
| path: ${{ env.XP_LIBS_DIR_WIN }} | |
| key: ${{ runner.os }}-xp-libs-${{ matrix.arch }}-${{ hashFiles('xp/build_libraries.ps1', 'xp/deps/**') }}-${{ github.run_id }} | |
| qt: | |
| name: XP Qt (${{ matrix.arch }}). | |
| needs: patches | |
| runs-on: windows-latest | |
| # A static Qt from scratch is hours. It only has to happen when the recipe or | |
| # the pin changes; every later run restores the cache and skips straight | |
| # past. Deliberately independent of the libraries job so both can run at once. | |
| timeout-minutes: 350 | |
| if: github.event_name != 'pull_request' && needs.patches.outputs.port == 'true' | |
| && (github.event.inputs.only == null || github.event.inputs.only == 'all' || github.event.inputs.only == 'qt') | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| arch: ${{ fromJSON(needs.patches.outputs.arches) }} | |
| env: | |
| XP_ARCH: ${{ matrix.arch }} | |
| # win32-msvc is the mkspec for both targets - Qt 5 takes the architecture | |
| # from the compiler - so only the prefix tells the two builds apart. | |
| XP_QT_PREFIX: ${{ matrix.arch == 'x64' && 'C:\xp-toolchain\qt-xp-static-prefix-x64' || 'C:\xp-toolchain\qt-xp-static-prefix' }} | |
| defaults: | |
| run: | |
| shell: pwsh | |
| steps: | |
| - name: Clone. | |
| uses: actions/checkout@v7 | |
| - name: Free up runner disk space. | |
| run: | | |
| function Show-Free($label) { | |
| Write-Host ("{0}: {1:N1} GB free on C:" -f $label, ((Get-PSDrive C).Free / 1GB)) | |
| } | |
| Show-Free 'before' | |
| foreach ($target in @( | |
| 'C:\Android', 'C:\Miniconda', 'C:\Julia', | |
| 'C:\Program Files\MongoDB', 'C:\Program Files\MySQL', | |
| 'C:\Program Files\PostgreSQL', 'C:\Program Files\Amazon', | |
| 'C:\Program Files\Microsoft SQL Server', | |
| 'C:\Program Files (x86)\Microsoft SQL Server', | |
| 'C:\Program Files (x86)\Google', 'C:\Program Files\R', | |
| 'C:\hostedtoolcache\windows\Java_Temurin-Hotspot_jdk', | |
| 'C:\hostedtoolcache\windows\Ruby', 'C:\hostedtoolcache\windows\go', | |
| 'C:\hostedtoolcache\windows\node', 'C:\hostedtoolcache\windows\PyPy')) { | |
| if (Test-Path $target) { | |
| Remove-Item -Recurse -Force $target -ErrorAction SilentlyContinue | |
| } | |
| } | |
| Show-Free 'after' | |
| - name: Qt cache. | |
| id: cache-qt | |
| uses: actions/cache/restore@v6 | |
| with: | |
| path: ${{ env.XP_QT_PREFIX }} | |
| key: ${{ runner.os }}-xp-qt-${{ matrix.arch }}-${{ hashFiles('xp/build_qt.ps1', 'xp/xp_env.ps1', 'xp/deps/qt5-xp.patch') }}-${{ github.run_id }} | |
| restore-keys: ${{ runner.os }}-xp-qt-${{ matrix.arch }}-${{ hashFiles('xp/build_qt.ps1', 'xp/xp_env.ps1', 'xp/deps/qt5-xp.patch') }}- | |
| - name: Bootstrap the XP toolchain. | |
| run: | | |
| & "$PWD\xp\bootstrap_toolchain.ps1" -Root C:\xp-toolchain -Arch ${{ matrix.arch }} | |
| if ($LASTEXITCODE -ne 0) { exit 1 } | |
| - name: Build Qt. | |
| run: | | |
| & "$PWD\xp\build_qt.ps1" -Root C:\xp-toolchain -Toolchain C:\xp-toolchain -Arch ${{ matrix.arch }} | |
| if ($LASTEXITCODE -ne 0) { exit 1 } | |
| - name: What got built. | |
| if: always() | |
| run: | | |
| $prefix = $env:XP_QT_PREFIX | |
| $expected = @('lib\Qt5Core.lib', 'lib\Qt5Gui.lib', 'lib\Qt5Widgets.lib', | |
| 'lib\Qt5Network.lib', 'lib\Qt5Svg.lib', 'lib\qtmain.lib', | |
| 'plugins\platforms\qwindows.lib', 'plugins\imageformats\qwebp.lib') | |
| $missing = 0 | |
| foreach ($rel in $expected) { | |
| $full = Join-Path $prefix $rel | |
| if (Test-Path $full) { | |
| Write-Host (" ok {0,12:N0} {1}" -f (Get-Item $full).Length, $rel) | |
| } else { | |
| Write-Host " MISSING $rel" | |
| $missing++ | |
| } | |
| } | |
| if ($missing -gt 0) { Write-Error "$missing Qt libraries missing"; exit 1 } | |
| - name: Save the Qt cache. | |
| if: always() | |
| uses: actions/cache/save@v6 | |
| with: | |
| path: ${{ env.XP_QT_PREFIX }} | |
| key: ${{ runner.os }}-xp-qt-${{ matrix.arch }}-${{ hashFiles('xp/build_qt.ps1', 'xp/xp_env.ps1', 'xp/deps/qt5-xp.patch') }}-${{ github.run_id }} | |
| build: | |
| name: XP ${{ matrix.arch }} Release. | |
| needs: [patches, libraries, qt] | |
| runs-on: windows-latest | |
| timeout-minutes: 350 | |
| # !cancelled() rather than the default: with `only`, libraries and qt are | |
| # SKIPPED, and a skipped dependency would skip this job too. Their caches | |
| # are what this stage actually consumes, so a skip is fine - a failure is not. | |
| if: >- | |
| !cancelled() | |
| && github.event_name != 'pull_request' | |
| && needs.patches.outputs.port == 'true' | |
| && needs.libraries.result != 'failure' | |
| && needs.qt.result != 'failure' | |
| && (github.event.inputs.only == null || github.event.inputs.only == 'all' || github.event.inputs.only == 'build') | |
| # NOTE: needs.<job>.result is the AGGREGATE of a matrix, so if either | |
| # architecture's libraries or Qt fails, neither build runs. That is the | |
| # conservative reading and usually the right one - the two share a recipe. | |
| # To carry on with the half that works, dispatch again with arch: x86 or | |
| # arch: x64, which reduces the matrix to one leg. | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| arch: ${{ fromJSON(needs.patches.outputs.arches) }} | |
| env: | |
| BUILD_DIR: out/ci-xp | |
| # Set once here instead of in five steps. cmake/variables.cmake reads the | |
| # last two straight out of the environment. | |
| XP_ARCH: ${{ matrix.arch }} | |
| XP_QT_PREFIX: ${{ matrix.arch == 'x64' && 'C:\xp-toolchain\qt-xp-static-prefix-x64' || 'C:\xp-toolchain\qt-xp-static-prefix' }} | |
| XP_LIBS_DIR_WIN: ${{ matrix.arch == 'x64' && 'C:\xp-toolchain\Libraries-x64' || 'C:\xp-toolchain\Libraries' }} | |
| XP_COMPAT_HEADER: ${{ github.workspace }}\xp\xp-compat.h | |
| defaults: | |
| run: | |
| shell: pwsh | |
| steps: | |
| - name: Clone. | |
| uses: actions/checkout@v7 | |
| with: | |
| submodules: recursive | |
| - name: Free up runner disk space. | |
| run: | | |
| foreach ($target in @( | |
| 'C:\Android', 'C:\Miniconda', 'C:\Julia', | |
| 'C:\Program Files\MongoDB', 'C:\Program Files\MySQL', | |
| 'C:\Program Files\PostgreSQL', 'C:\Program Files\Amazon', | |
| 'C:\Program Files\Microsoft SQL Server', | |
| 'C:\Program Files (x86)\Microsoft SQL Server', | |
| 'C:\Program Files (x86)\Google', 'C:\Program Files\R', | |
| 'C:\hostedtoolcache\windows\Java_Temurin-Hotspot_jdk', | |
| 'C:\hostedtoolcache\windows\Ruby', 'C:\hostedtoolcache\windows\go', | |
| 'C:\hostedtoolcache\windows\node', 'C:\hostedtoolcache\windows\PyPy')) { | |
| if (Test-Path $target) { Remove-Item -Recurse -Force $target -ErrorAction SilentlyContinue } | |
| } | |
| Write-Host ("C: {0:N1} GB free" -f ((Get-PSDrive C).Free / 1GB)) | |
| - name: Bootstrap the XP toolchain. | |
| run: | | |
| & "$PWD\xp\bootstrap_toolchain.ps1" -Root C:\xp-toolchain -Arch ${{ matrix.arch }} | |
| if ($LASTEXITCODE -ne 0) { exit 1 } | |
| - name: Libraries cache. | |
| uses: actions/cache/restore@v6 | |
| with: | |
| path: ${{ env.XP_LIBS_DIR_WIN }} | |
| key: ${{ runner.os }}-xp-libs-${{ matrix.arch }}-${{ hashFiles('xp/build_libraries.ps1', 'xp/deps/**') }}- | |
| fail-on-cache-miss: true | |
| - name: Qt cache. | |
| uses: actions/cache/restore@v6 | |
| with: | |
| path: ${{ env.XP_QT_PREFIX }} | |
| key: ${{ runner.os }}-xp-qt-${{ matrix.arch }}-${{ hashFiles('xp/build_qt.ps1', 'xp/xp_env.ps1', 'xp/deps/qt5-xp.patch') }}- | |
| fail-on-cache-miss: true | |
| # cmake/variables.cmake resolves the dependencies as a SIBLING of the source | |
| # tree (../Libraries-walk), the way the workstation is laid out. A junction | |
| # keeps that rule intact without teaching CMake about the runner's paths. | |
| - name: Put the libraries where CMake looks for them. | |
| run: | | |
| $parent = Split-Path $env:GITHUB_WORKSPACE -Parent | |
| $sibling = Join-Path $parent 'Libraries-walk' | |
| # The junction is what makes one CMake rule ("../Libraries-walk") serve | |
| # both architectures: the name stays put, the target moves. | |
| if (-not (Test-Path $sibling)) { | |
| New-Item -ItemType Junction -Path $sibling -Target $env:XP_LIBS_DIR_WIN | Out-Null | |
| } | |
| Write-Host "$sibling -> $env:XP_LIBS_DIR_WIN" | |
| # ../Libraries has to exist as well, even empty. DESKTOP_APP_USE_PACKAGED | |
| # is a cmake_dependent_option keyed on its presence, and when the | |
| # condition is false the option is FORCED to ON - a -D on the command | |
| # line cannot override that, and every external_* target then goes | |
| # looking for system packages that are not there. | |
| $packagedProbe = Join-Path $parent 'Libraries' | |
| if (-not (Test-Path $packagedProbe)) { | |
| New-Item -ItemType Directory -Path $packagedProbe | Out-Null | |
| } | |
| Write-Host "$packagedProbe exists: $(Test-Path $packagedProbe)" | |
| # The root CMakeLists points Python3_EXECUTABLE at ../ThirdParty/python/ | |
| # Scripts/python - tdesktop's own layout, a virtualenv - and does it with | |
| # a plain set(), so it overrides anything passed with -D. Give it exactly | |
| # that: a venv in exactly that place. | |
| $venv = Join-Path $parent 'ThirdParty\python' | |
| if (-not (Test-Path (Join-Path $venv 'Scripts\python.exe'))) { | |
| python -m venv $venv | |
| if ($LASTEXITCODE -ne 0) { Write-Error 'creating the python venv failed'; exit 1 } | |
| } | |
| & (Join-Path $venv 'Scripts\python.exe') --version | |
| Get-ChildItem $sibling | Select-Object -ExpandProperty Name | |
| - name: Build tree cache. | |
| uses: actions/cache/restore@v6 | |
| with: | |
| path: ${{ env.BUILD_DIR }} | |
| key: ${{ runner.os }}-xp-out-${{ matrix.arch }}-${{ github.sha }} | |
| restore-keys: ${{ runner.os }}-xp-out-${{ matrix.arch }}- | |
| # packer.cpp includes these two by relative path from OUTSIDE the checkout, | |
| # the way tdesktop has always expected DesktopPrivate to sit next to it. The | |
| # private key never reaches the disk of anything but this runner, and the | |
| # public half it must pair with is committed in packer.cpp / config.h. | |
| # Empty placeholders keep a secret-less fork compiling; packing then fails | |
| # at the signature check rather than silently shipping an unsigned update. | |
| - name: Set up DesktopPrivate. | |
| env: | |
| PACKER_KEY: ${{ secrets.PACKER_PRIVATE_KEY }} | |
| PACKER_BETA_KEY: ${{ secrets.PACKER_PRIVATE_BETA_KEY }} | |
| ALPHA_KEY: ${{ secrets.ALPHA_PRIVATE_KEY }} | |
| run: | | |
| $private = Join-Path (Split-Path $PWD -Parent) 'DesktopPrivate' | |
| New-Item -ItemType Directory -Force -Path $private | Out-Null | |
| function Literal($name, $pem, $static) { | |
| $lines = @($pem -split "`r?`n" | Where-Object { $_.Trim() }) | |
| $body = '' | |
| for ($i = 0; $i -lt $lines.Count; $i++) { | |
| $body += $lines[$i].Trim() + $(if ($i -lt $lines.Count - 1) { '\n\' } else { '\' }) + "`n" | |
| } | |
| $prefix = if ($static) { '[[maybe_unused]] static ' } else { '' } | |
| return "${prefix}const char *${name} = `"\`n${body}`";`n" | |
| } | |
| $packer = if ($env:PACKER_KEY) { Literal 'PrivateKey' $env:PACKER_KEY $false } else { "const char *PrivateKey = `"`";`n" } | |
| $beta = if ($env:PACKER_BETA_KEY) { Literal 'PrivateBetaKey' $env:PACKER_BETA_KEY $false } else { "const char *PrivateBetaKey = `"`";`n" } | |
| $alpha = if ($env:ALPHA_KEY) { Literal 'AlphaPrivateKey' $env:ALPHA_KEY $true } else { "[[maybe_unused]] static const char *AlphaPrivateKey = `"`";`n" } | |
| Set-Content -Path (Join-Path $private 'packer_private.h') -Value ($packer + "`n" + $beta) -Encoding ascii | |
| Set-Content -Path (Join-Path $private 'alpha_private.h') -Value $alpha -Encoding ascii | |
| Write-Host ("signing keys: packer {0}, beta {1}, alpha {2}" -f ` | |
| $(if ($env:PACKER_KEY) { 'present' } else { 'ABSENT' }), | |
| $(if ($env:PACKER_BETA_KEY) { 'present' } else { 'ABSENT' }), | |
| $(if ($env:ALPHA_KEY) { 'present' } else { 'ABSENT' })) | |
| # A key pair that does not match is invisible until the Packer refuses its | |
| # own output - two hours into the build, at the very last step. The public | |
| # half is public by definition, so derive it here and print both sides. | |
| - name: The signing key matches the compiled-in public key. | |
| if: inputs.publish_update != 'off' && inputs.publish_update != '' | |
| env: | |
| PACKER_KEY: ${{ secrets.PACKER_PRIVATE_KEY }} | |
| run: | | |
| if (-not $env:PACKER_KEY) { | |
| Write-Error 'PACKER_PRIVATE_KEY is empty - nothing can be signed.' | |
| exit 1 | |
| } | |
| $rsa = [System.Security.Cryptography.RSA]::Create() | |
| try { | |
| $rsa.ImportFromPem($env:PACKER_KEY) | |
| } catch { | |
| Write-Error "PACKER_PRIVATE_KEY is not a PEM private key: $_" | |
| exit 1 | |
| } | |
| $derived = [Convert]::ToBase64String($rsa.ExportRSAPublicKey()) | |
| # The compiled-in key is a C string literal split over lines with \n\. | |
| $source = Get-Content "$PWD\Telegram\SourceFiles\_other\packer.cpp" -Raw | |
| $match = [regex]::Match($source, '(?s)const char \*PublicKey = "\\\r?\n(.*?)";') | |
| if (-not $match.Success) { Write-Error 'no PublicKey literal in packer.cpp'; exit 1 } | |
| $compiled = ($match.Groups[1].Value -replace '\\n\\', '' -replace '\\', '' ` | |
| -replace '-----[A-Z ]+-----', '' -replace '\s', '') | |
| if ($derived -ne $compiled) { | |
| Write-Host "derived from the secret : $derived" | |
| Write-Host "compiled into the build : $compiled" | |
| Write-Error ('PACKER_PRIVATE_KEY does not pair with the public key in ' + | |
| 'packer.cpp / config.h. Either point the secret at the key those were ' + | |
| 'taken from, or commit the public half of this one.') | |
| exit 1 | |
| } | |
| Write-Host "signing key pairs with the compiled-in public key." | |
| - name: Configure. | |
| env: | |
| API_ID: ${{ secrets.TDESKTOP_API_ID }} | |
| API_HASH: ${{ secrets.TDESKTOP_API_HASH }} | |
| run: | | |
| # A build with the wrong API credentials looks perfect - it compiles, | |
| # passes every gate, starts on XP - and then never connects, which is | |
| # only visible to a human in front of the machine. So refuse to produce | |
| # one. (Telegram publishes 17349 for open source builds; it is rate | |
| # limited to uselessness, so it is not a fallback worth having here.) | |
| if (-not $env:API_ID -or -not $env:API_HASH) { | |
| Write-Error ('TDESKTOP_API_ID / TDESKTOP_API_HASH are not set in this ' + | |
| 'repository. A build made without them cannot log in - see ' + | |
| 'https://core.telegram.org/api/obtaining_api_id') | |
| exit 1 | |
| } | |
| $id = $env:API_ID | |
| $hash = $env:API_HASH | |
| Write-Host "API credentials: from repository secrets (id $($id.Length) chars, hash $($hash.Length) chars)." | |
| if ($hash.Length -ne 32) { | |
| Write-Error "TDESKTOP_API_HASH must be 32 hex characters, this one is $($hash.Length)." | |
| exit 1 | |
| } | |
| # XP_QT_PREFIX, XP_COMPAT_HEADER and XP_ARCH come from the job env now: | |
| # two of the three are per-architecture, and cmake/variables.cmake | |
| # reads them straight out of the environment. | |
| # DESKTOP_APP_USE_PACKAGED turns itself ON when no ../Libraries directory | |
| # sits next to the checkout - exactly the case on a runner - and every | |
| # external_* target then switches to find_package(). This port supplies | |
| # its dependencies by path, so pin the switch off. | |
| # Autoupdate is OFF by default for a target with no DESKTOP_APP_SPECIAL_ | |
| # TARGET, which is what this port is. Turn it on explicitly rather than | |
| # claiming a special target - that would also switch on /GL + /LTCG and | |
| # flip the crash-report default. BUILD_PACKER is this port's own option | |
| # for the same reason. | |
| & "$PWD\xp\cmake_xp.ps1" cmake -G Ninja ` | |
| -D CMAKE_BUILD_TYPE=Release ` | |
| -D DESKTOP_APP_USE_PACKAGED=OFF ` | |
| -D DESKTOP_APP_DISABLE_AUTOUPDATE=OFF ` | |
| -D DESKTOP_APP_BUILD_PACKER=ON ` | |
| -D TDESKTOP_API_ID=$id ` | |
| -D TDESKTOP_API_HASH=$hash ` | |
| -S . -B $env:BUILD_DIR | |
| if ($LASTEXITCODE -ne 0) { exit 1 } | |
| # The code generators are host tools built with the very same XP toolchain, | |
| # so they have to RUN here, on the runner. Build and exercise one first: | |
| # when it dies the ninja log only shows an exit code, and 0xC0000005 from a | |
| # generator is indistinguishable from a compile error at that level. | |
| # Does anything this toolchain produces run here at all? The code | |
| # generators are XP-targeted binaries that have to execute on the runner, | |
| # so separate "the toolchain cannot run here" from "this particular | |
| # generator crashed" before reading anything into a ninja exit code. | |
| - name: Can an XP binary run on the runner? | |
| run: | | |
| $repoRoot = $PWD.Path | |
| $probe = Join-Path $env:RUNNER_TEMP 'runprobe' | |
| New-Item -ItemType Directory -Force -Path $probe | Out-Null | |
| # Streams, not printf: the generators crash inside the CRT's locale | |
| # setup on the first use of a C++ stream, so the probe has to reach the | |
| # same code. Built twice - with fpcompat and without - because that | |
| # library carries objects lifted from a DIFFERENT CRT version, which is | |
| # the obvious suspect for a locale structure being torn. | |
| @' | |
| #include <windows.h> | |
| #include <iostream> | |
| #include <sstream> | |
| #include <string> | |
| #include <vector> | |
| int main() { | |
| std::vector<std::string> v{ "xp", "runs", "here" }; | |
| std::ostringstream out; | |
| for (const auto &s : v) { out << s << ' '; } | |
| std::cout << out.str() << "(" << out.str().size() << ")" << std::endl; | |
| return 0; | |
| } | |
| '@ | Set-Content -Path "$probe\runprobe.cpp" -Encoding ascii | |
| & "$repoRoot\xp\xp_env.ps1" -Toolchain C:\xp-toolchain -Quiet | |
| Push-Location $probe | |
| & cl.exe /nologo /MT /EHsc /d2FH4- /FI"$repoRoot\xp\xp-compat.h" /c runprobe.cpp 2>&1 | Write-Host | |
| & link.exe /nologo "/SUBSYSTEM:CONSOLE,$env:XP_SUBSYSTEM_VERSION" /FORCE:MULTIPLE runprobe.obj ` | |
| "$env:XP_FPCOMPAT\fpcompat.lib" /OUT:with_fpcompat.exe 2>&1 | Write-Host | |
| & link.exe /nologo "/SUBSYSTEM:CONSOLE,$env:XP_SUBSYSTEM_VERSION" runprobe.obj /OUT:without_fpcompat.exe 2>&1 | Write-Host | |
| Pop-Location | |
| & "$probe\with_fpcompat.exe" | |
| Write-Host "with fpcompat: exit code $LASTEXITCODE" | |
| & "$probe\without_fpcompat.exe" | |
| Write-Host "without fpcompat: exit code $LASTEXITCODE" | |
| # Same program plus Qt5Core - the one thing the code generators have | |
| # that these probes do not. If this crashes the problem is the Qt build; | |
| # if it runs, it is how the generator itself is put together. | |
| $qt = $env:XP_QT_PREFIX | |
| @' | |
| #include <QtCore/QString> | |
| #include <QtCore/QStringList> | |
| #include <iostream> | |
| int main() { | |
| QStringList parts; | |
| parts << QString::fromLatin1("qt") << QString::fromLatin1("probe"); | |
| std::cout << parts.join(QChar(' ')).toStdString() << std::endl; | |
| return 0; | |
| } | |
| '@ | Set-Content -Path "$probe\qtprobe.cpp" -Encoding ascii | |
| Push-Location $probe | |
| & cl.exe /nologo /MT /EHsc /d2FH4- /FI"$repoRoot\xp\xp-compat.h" ` | |
| /I"$qt\include" /I"$qt\include\QtCore" /DQT_STATIC /c qtprobe.cpp 2>&1 | Write-Host | |
| if ($LASTEXITCODE -eq 0) { | |
| & link.exe /nologo "/SUBSYSTEM:CONSOLE,$env:XP_SUBSYSTEM_VERSION" /FORCE:MULTIPLE qtprobe.obj ` | |
| "$qt\lib\Qt5Core.lib" "$qt\lib\qtpcre2.lib" "$env:XP_FPCOMPAT\fpcompat.lib" ` | |
| ws2_32.lib advapi32.lib shell32.lib ole32.lib user32.lib winmm.lib ` | |
| netapi32.lib userenv.lib version.lib mpr.lib /OUT:qtprobe.exe 2>&1 | Write-Host | |
| } | |
| Pop-Location | |
| if (Test-Path "$probe\qtprobe.exe") { | |
| & "$probe\qtprobe.exe" | |
| Write-Host "qt probe: exit code $LASTEXITCODE" | |
| } else { | |
| Write-Host 'qt probe did not build' | |
| } | |
| # Which C runtime does Qt ask for? A static build must request LIBCMT; if | |
| # any of it came out /MD the final image ends up with two CRT instances and | |
| # the second one's locale state is what a std::cerr initializer trips over. | |
| - name: Which CRT does Qt want? | |
| run: | | |
| & "$PWD\xp\xp_env.ps1" -Toolchain C:\xp-toolchain -Quiet | |
| foreach ($lib in @('Qt5Core.lib', 'qtpcre2.lib')) { | |
| $path = "$env:XP_QT_PREFIX\lib\$lib" | |
| if (-not (Test-Path $path)) { Write-Host "$lib missing"; continue } | |
| $directives = & dumpbin.exe /nologo /directives $path 2>&1 | | |
| Select-String -Pattern 'DEFAULTLIB:"?(lib)?(cmt|cpmt|ucrt|vcruntime|msvcrt|msvcprt)' | | |
| ForEach-Object { $_.ToString().Trim() } | Sort-Object -Unique | |
| Write-Host "$lib wants:" | |
| $directives | Select-Object -First 12 | ForEach-Object { Write-Host " $_" } | |
| } | |
| - name: Code generators. | |
| run: | | |
| # XP_QT_PREFIX, XP_COMPAT_HEADER and XP_ARCH come from the job env now: | |
| # two of the three are per-architecture, and cmake/variables.cmake | |
| # reads them straight out of the environment. | |
| & "$PWD\xp\cmake_xp.ps1" ninja -C $env:BUILD_DIR codegen_lang | |
| if ($LASTEXITCODE -ne 0) { Write-Error 'building codegen_lang failed'; exit 1 } | |
| $tool = Get-ChildItem -Recurse -Filter codegen_lang.exe $env:BUILD_DIR | | |
| Select-Object -First 1 | |
| Write-Host "tool: $($tool.FullName)" | |
| & "$PWD\xp\cmake_xp.ps1" dumpbin /dependents $tool.FullName | | |
| Select-String -Pattern '\.dll' | ForEach-Object { Write-Host " $_" } | |
| # No arguments: it must print its usage and exit cleanly. A crash here | |
| # is the toolchain, not the codegen input. | |
| & $tool.FullName 2>&1 | Select-Object -First 5 | ForEach-Object { Write-Host " $_" } | |
| Write-Host "exit code: $LASTEXITCODE" | |
| # Without arguments the generator prints usage and returns non-zero, | |
| # which is fine - only a CRASH matters here. Windows reports those as | |
| # 0xC0000000-range codes, negative once PowerShell has signed them. | |
| $crashed = ($LASTEXITCODE -lt -1000000) | |
| if (-not $crashed) { Write-Host 'the generator runs on this machine.' } | |
| # An exit code is not a diagnosis. The Windows kit ships cdb; run the | |
| # tool under it and print the faulting frame, which is the only thing | |
| # that says WHERE a 0xC0000005 comes from. | |
| if ($crashed) { | |
| # The debugger has to match the bitness of the process it opens. | |
| $cdb = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\Debuggers\$env:XP_ARCH\cdb.exe" -ErrorAction SilentlyContinue | | |
| Select-Object -First 1 | |
| if (-not $cdb) { | |
| $cdb = Get-ChildItem "C:\Program Files*\Windows Kits\10\Debuggers\*\cdb.exe" -ErrorAction SilentlyContinue | | |
| Select-Object -First 1 | |
| } | |
| if ($cdb) { | |
| Write-Host "debugger: $($cdb.FullName)" | |
| & $cdb.FullName -g -G -c '.lastevent; kb 40; q' $tool.FullName 2>&1 | | |
| Select-Object -Last 70 | ForEach-Object { Write-Host " $_" } | |
| } else { | |
| Write-Host 'cdb not found on this image' | |
| } | |
| Write-Error 'the code generator cannot run here' | |
| exit 1 | |
| } | |
| # The usage exit above is the last command's code, and PowerShell would | |
| # otherwise hand it to Actions as the step result. | |
| exit 0 | |
| - name: Build. | |
| run: | | |
| # XP_QT_PREFIX, XP_COMPAT_HEADER and XP_ARCH come from the job env now: | |
| # two of the three are per-architecture, and cmake/variables.cmake | |
| # reads them straight out of the environment. | |
| # Updater.exe applies an update, Packer.exe signs one. Both come from | |
| # the same toolchain as the app and are cheap next to it. | |
| & "$PWD\xp\cmake_xp.ps1" ninja -C $env:BUILD_DIR Telegram Updater Packer | |
| if ($LASTEXITCODE -ne 0) { exit 1 } | |
| - name: Save the build tree cache. | |
| if: always() | |
| uses: actions/cache/save@v6 | |
| with: | |
| path: ${{ env.BUILD_DIR }} | |
| key: ${{ runner.os }}-xp-out-${{ matrix.arch }}-${{ github.sha }} | |
| # The gate that matters: a binary that links can still be unable to start on | |
| # XP, through an absent DLL or a Vista+ entry point. Never publish without it. | |
| - name: XP safety. | |
| run: | | |
| # No -Dumpbin: XP_TOOLSET_BINARY_DIR belongs to the process xp_env.ps1 | |
| # set up, and every step here is a fresh one, so passing it produced an | |
| # empty path. xpsafe.ps1 finds dumpbin itself. | |
| & "$PWD\xp\xpsafe.ps1" -Exe "$PWD\$env:BUILD_DIR\Telegram.exe" | |
| if ($LASTEXITCODE -ne 0) { Write-Error 'the binary cannot start on XP'; exit 1 } | |
| # Updater.exe is what REPLACES the running application, so an unsafe one | |
| # turns a working install into a dead one with no way back. It ships in | |
| # the update package, so it is gated exactly like the app. | |
| & "$PWD\xp\xpsafe.ps1" -Exe "$PWD\$env:BUILD_DIR\Updater.exe" | |
| if ($LASTEXITCODE -ne 0) { Write-Error 'the updater cannot start on XP'; exit 1 } | |
| # The runtime half of the gate. xpsafe above proves the binary can START on | |
| # XP; this runs the subsystems that have only ever failed later - FFmpeg | |
| # registration and its codec set, the opus encoder's sample format, rlottie, | |
| # the static image plugins. Most of that is not XP-specific, so the runner | |
| # catches it minutes after the link instead of a human catching it in front | |
| # of the VM. Exit code 3 means a probe hung and its watchdog fired. | |
| - name: Self-test. | |
| run: | | |
| $report = Join-Path $env:RUNNER_TEMP 'xpselftest.txt' | |
| $process = Start-Process -PassThru -FilePath "$PWD\$env:BUILD_DIR\Telegram.exe" ` | |
| -ArgumentList '-xpselftest', "`"$report`"" | |
| # Touching Handle keeps the process object able to report ExitCode after | |
| # it dies; without it PowerShell can hand back an empty exit code and the | |
| # gate would pass a failing self-test. | |
| $null = $process.Handle | |
| if (-not $process.WaitForExit(300000)) { | |
| $process.Kill() | |
| if (Test-Path $report) { Get-Content $report } | |
| Write-Error 'the self-test did not finish in 5 minutes' | |
| exit 1 | |
| } | |
| if (Test-Path $report) { Get-Content $report } else { Write-Host 'no report written' } | |
| if ($process.ExitCode -ne 0) { | |
| Write-Error "the self-test failed with exit code $($process.ExitCode)" | |
| exit 1 | |
| } | |
| - name: Read version. | |
| shell: bash | |
| run: awk 'NF { print $1"="$2 }' Telegram/build/version >> $GITHUB_ENV | |
| - name: Package. | |
| run: | | |
| $name = "Telegram_XP_$env:AppVersionStr" + "_$env:XP_ARCH" | |
| $stage = Join-Path $env:RUNNER_TEMP $name | |
| New-Item -ItemType Directory -Force -Path $stage | Out-Null | |
| # Fully static, so the app is one file - plus Updater.exe, which is what | |
| # applies a downloaded update. Without it next to the app the update is | |
| # fetched and verified and then simply never installed. | |
| Copy-Item "$PWD\$env:BUILD_DIR\Telegram.exe" $stage | |
| Copy-Item "$PWD\$env:BUILD_DIR\Updater.exe" $stage | |
| $zip = Join-Path $env:RUNNER_TEMP "$name.zip" | |
| if (Test-Path $zip) { Remove-Item $zip } | |
| Compress-Archive -Path "$stage\*" -DestinationPath $zip | |
| echo "ARCHIVE=$zip" >> $env:GITHUB_ENV | |
| Write-Host ("Telegram.exe {0:N0} + Updater.exe {1:N0} bytes -> {2:N0} zipped" -f ` | |
| (Get-Item "$PWD\$env:BUILD_DIR\Telegram.exe").Length, ` | |
| (Get-Item "$PWD\$env:BUILD_DIR\Updater.exe").Length, (Get-Item $zip).Length) | |
| - uses: actions/upload-artifact@v7 | |
| name: Upload the artifact. | |
| with: | |
| name: Telegram XP ${{ matrix.arch }} ${{ env.AppVersionStr }} | |
| path: ${{ env.ARCHIVE }} | |
| # Always: it is small, and without it a stack captured from a hung build | |
| # on XP can only ever be read as module+offset. The PDB would answer the | |
| # same question, but it is 2.5 GB - more to move than the build itself. | |
| - uses: actions/upload-artifact@v7 | |
| name: Upload the link map. | |
| with: | |
| name: Telegram map ${{ matrix.arch }} ${{ env.AppVersionStr }} | |
| path: ${{ env.BUILD_DIR }}/Telegram.map | |
| # An update package is Telegram.exe + Updater.exe, compressed and signed | |
| # with the key set up above; the client refuses anything whose signature | |
| # does not match the public key committed in config.h. Packer verifies its | |
| # own output before writing it, so a key mismatch fails here and not on a | |
| # user's machine. The binaries going in are the ones xpsafe just cleared. | |
| - name: Pack the update. | |
| if: inputs.publish_update != 'off' && inputs.publish_update != '' | |
| run: | | |
| $release = "$PWD\$env:BUILD_DIR" | |
| Set-Location $release | |
| # -target winxp names the file txpupd<version>: the XP build is x86 like | |
| # the plain `win` target and would otherwise claim its name in a feed | |
| # shared with the Windows 7+ releases. winxp64 -> txp64upd<version> for | |
| # exactly the same reason against `win64` / tx64upd. | |
| $target = if ($env:XP_ARCH -eq 'x64') { 'winxp64' } else { 'winxp' } | |
| $prefix = if ($env:XP_ARCH -eq 'x64') { 'txp64upd' } else { 'txpupd' } | |
| & .\Packer.exe -version $env:AppVersion -path Telegram.exe -path Updater.exe -target $target | |
| if ($LASTEXITCODE -ne 0) { Write-Error 'packing failed'; exit 1 } | |
| $update = "$prefix$env:AppVersion" | |
| if (-not (Test-Path $update)) { Write-Error "$update was not produced"; exit 1 } | |
| Write-Host ("$update {0:N0} bytes" -f (Get-Item $update).Length) | |
| echo "UPDATE_FILE=$release\$update" >> $env:GITHUB_ENV | |
| - uses: actions/upload-artifact@v7 | |
| name: Upload the update package. | |
| if: inputs.publish_update != 'off' && inputs.publish_update != '' | |
| with: | |
| name: XP update ${{ matrix.arch }} ${{ env.AppVersionStr }} | |
| path: ${{ env.UPDATE_FILE }} | |
| - name: Publish the release. | |
| if: inputs.publish_release || startsWith(github.ref, 'refs/tags/xp-v') | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ secrets.RELEASE_TOKEN || github.token }} | |
| run: | | |
| set -e | |
| R="$GITHUB_REPOSITORY" | |
| if [[ "$GITHUB_REF" == refs/tags/xp-v* ]]; then | |
| TAG="${GITHUB_REF#refs/tags/}" | |
| TARGET=() | |
| else | |
| TAG="xp-v$AppVersionStr" | |
| TARGET=(--target "$GITHUB_SHA") | |
| fi | |
| if ! gh release view "$TAG" -R "$R" >/dev/null 2>&1; then | |
| # Both architectures reach this at the same time, so losing the race | |
| # is the normal case, not an error: create, and if that failed only | |
| # because the other leg got there first, carry on. A real failure | |
| # still stops the job, because the view below fails too. | |
| gh release create "$TAG" -R "$R" "${TARGET[@]}" --draft \ | |
| --title "Telegram Desktop $AppVersionStr for Windows XP" \ | |
| --notes "Windows XP builds of Telegram Desktop $AppVersionStr. | |
| _x86 Windows XP SP3, 32-bit. | |
| _x64 Windows XP Professional x64 Edition (NT 5.2). Also runs the x86 | |
| build through WOW64 - but the two update through separate feed | |
| keys, so install the one you mean to keep. | |
| A single static executable each. Voice, video and group calls are absent - | |
| they need WebRTC, which cannot run on XP. xp/README.md lists what else the | |
| platform limits and how the builds are produced." \ | |
| || gh release view "$TAG" -R "$R" >/dev/null | |
| fi | |
| gh release upload "$TAG" -R "$R" "$ARCHIVE" --clobber | |
| publish-update: | |
| name: Publish the update to Telegram. | |
| needs: build | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| if: always() | |
| && github.event_name == 'workflow_dispatch' | |
| && github.event.inputs.publish_update != 'off' | |
| && github.event.inputs.publish_update != 'package' | |
| && github.event.inputs.publish_update != '' | |
| && (needs.build.result == 'success' | |
| || github.event.inputs.publish_from_run != '') | |
| # The session string reaches only this job, and only after whoever reviews | |
| # the environment approves it. | |
| environment: telegram-publish | |
| steps: | |
| - name: Clone the publisher. | |
| uses: actions/checkout@v7 | |
| with: | |
| sparse-checkout: xp/publish_telegram.py | |
| sparse-checkout-cone-mode: false | |
| # Either the package this run just built, or the one an earlier run did - | |
| # a republish then costs a minute instead of a two-hour rebuild. | |
| - name: Download the update package. | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| FROM_RUN: ${{ github.event.inputs.publish_from_run }} | |
| run: | | |
| set -e | |
| mkdir -p artifacts | |
| RUN="${FROM_RUN:-$GITHUB_RUN_ID}" | |
| echo "taking the package from run $RUN" | |
| gh run download "$RUN" -R "$GITHUB_REPOSITORY" -D artifacts | |
| find artifacts -type f -printf '%p\t%s bytes\n' | |
| - uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.12" | |
| - name: Install Telethon. | |
| run: pip install telethon | |
| # The feed is the same message the Windows 7+ and Linux releases write, so | |
| # the script merges onto it instead of replacing it, and touches only the | |
| # winxp and winxp64 keys. It publishes whichever of the two packages this | |
| # run produced - both, when the matrix built both - in ONE feed message. | |
| # 'rehearsal' sends everything for real but 360 days out: the whole path | |
| # runs, nothing appears in the channels. Delete those afterwards. | |
| - name: Publish. | |
| env: | |
| TG_API_ID: "611335" | |
| TG_API_HASH: "d524b414d21f4d37f08684c1df41ac9c" | |
| TG_SESSION: ${{ secrets.TG_SESSION }} | |
| TG_FEED_CHANNEL: ${{ secrets.TG_FEED_CHANNEL }} | |
| TG_FILES_CHANNEL: ${{ secrets.TG_FILES_CHANNEL }} | |
| ARTIFACTS_DIR: artifacts | |
| TG_SCHEDULE_DAYS: ${{ inputs.publish_update == 'rehearsal' && '360' || '0' }} | |
| TG_ENTRY_KEY: ${{ inputs.publish_update == 'testing' && 'testing' || 'released' }} | |
| run: python xp/publish_telegram.py |