ZaStoGram_desktop/.github/workflows/win.yml
loop-uh 5f69581fec Say which ZaStoGram build you are actually running
The side menu called the app Telegram Desktop and showed only the base
version number, so an installed dev build looked exactly like the stable
release it was built from. It now names ZaStoGram and carries the
pre-release number CI built it as.
2026-07-27 23:26:02 +03:00

887 lines
38 KiB
YAML

name: Windows.
on:
push:
paths-ignore:
- 'docs/**'
- '**.md'
- '!docs/building-win*.md'
- 'changelog.txt'
- 'LEGAL'
- 'LICENSE'
- '.github/**'
- '!.github/workflows/win.yml'
- 'lib/xdg/**'
- 'snap/**'
- 'Telegram/build/docker/**'
- 'Telegram/Resources/uwp/**'
- 'Telegram/SourceFiles/platform/linux/**'
- 'Telegram/SourceFiles/platform/mac/**'
- 'Telegram/Telegram/**'
- 'Telegram/configure.sh'
- 'Telegram/Telegram.plist'
pull_request:
paths-ignore:
- 'docs/**'
- '**.md'
- '!docs/building-win*.md'
- 'changelog.txt'
- 'LEGAL'
- 'LICENSE'
- '.github/**'
- '!.github/workflows/win.yml'
- 'lib/xdg/**'
- 'snap/**'
- 'Telegram/build/docker/**'
- 'Telegram/Resources/uwp/**'
- 'Telegram/SourceFiles/platform/linux/**'
- 'Telegram/SourceFiles/platform/mac/**'
- 'Telegram/Telegram/**'
- 'Telegram/configure.sh'
- 'Telegram/Telegram.plist'
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name }}
cancel-in-progress: true
jobs:
windows:
name: Windows
runs-on: ${{ matrix.arch == 'arm64' && 'windows-11-arm' || ((github.event_name == 'pull_request' || github.ref == 'refs/heads/nightly') && 'depot-windows-latest-16' || 'windows-latest') }}
permissions:
contents: read
actions: write # prune superseded cache entries
strategy:
matrix:
# x64 builds with Qt 6 to get the QRhi (D3D11) GPU rendering path;
# the 32-bit build stays on Qt 5 for older Windows versions.
include:
- arch: x64_x86
qt: ""
generator: ""
- arch: x64
qt: "qt6"
generator: ""
env:
UPLOAD_ARTIFACT: "true"
ONLY_CACHE: "false"
PREPARE_PATH: "Telegram/build/prepare/prepare.py"
# CACHE_KEY (in "First set up") auto-hashes prepare.py recipes + win.bat + SDK.
# CACHE_VERSION is a manual kill-switch: bump it to invalidate ALL caches at
# once for changes that are NOT auto-detected — the disk-trim filter or the
# cache path lists below, and as a safety net for a toolset bump.
CACHE_VERSION: "v1"
# LIBS_CACHE_VERSION is the same kill-switch, narrowed to the Libraries
# tarball, so a trim-filter change that only invalidates library artifacts
# does not also throw away the (far more expensive) Qt cache.
LIBS_CACHE_VERSION: "v2"
TELEGRAM_BUILD_CACHE_VERSION: "v3"
SCCACHE_GHA_ENABLED: "true"
SCCACHE_IGNORE_SERVER_IO_ERROR: "1"
SCCACHE_IDLE_TIMEOUT: "600"
SCCACHE_LOG: warn
defaults:
run:
shell: cmd
steps:
- name: Prepare directories.
run: |
mkdir %userprofile%\TBuild\Libraries
mklink /d %GITHUB_WORKSPACE%\TBuild %userprofile%\TBuild
echo TBUILD=%GITHUB_WORKSPACE%\TBuild>>%GITHUB_ENV%
echo LibrariesPath=%GITHUB_WORKSPACE%\TBuild\Libraries${{ matrix.arch == 'x64' && '\win64' || '' }}>>%GITHUB_ENV%
- name: Get repository name.
shell: bash
run: echo "REPO_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV
- name: Clone.
uses: actions/checkout@v7
with:
submodules: recursive
path: ${{ env.TBUILD }}\${{ env.REPO_NAME }}
- name: First set up.
shell: bash
run: |
DOCPATH=$TBUILD/$REPO_NAME/docs/building-win.md
SDK="$(grep "SDK version" $DOCPATH | sed -r 's/.*\*\*(.*)\*\* SDK version.*/\1/')"
echo "SDK=$SDK" >> $GITHUB_ENV
printf 'SCCACHE_BASEDIRS=%s\\%s\n' "$TBUILD" "$REPO_NAME" >> $GITHUB_ENV
printf 'SCCACHE_GHA_VERSION=%s\n' "${{ runner.OS }}-${{ matrix.arch }}-${{ matrix.qt || 'qt' }}-${SDK}-${{ matrix.defines || 'default' }}-msvc-v1" >> $GITHUB_ENV
echo "SCCACHE_ERROR_LOG=$RUNNER_TEMP/sccache_log.txt" >> $GITHUB_ENV
case "${{ matrix.arch }}" in
x64_x86) SCCACHE_PORT_OFFSET=0 ;;
x64) SCCACHE_PORT_OFFSET=1 ;;
arm64) SCCACHE_PORT_OFFSET=2 ;;
*) SCCACHE_PORT_OFFSET=3 ;;
esac
SCCACHE_SERVER_PORT=$(( 4200 + (GITHUB_RUN_ID % 1000) * 4 + SCCACHE_PORT_OFFSET ))
echo "SCCACHE_SERVER_PORT=$SCCACHE_SERVER_PORT" >> $GITHUB_ENV
sed -i '/CMAKE_${lang}_FLAGS_DEBUG_INIT/s/${_Zi}//' "$PROGRAMFILES"/CMake/share/cmake*/Modules/Platform/Windows-MSVC.cmake
# CACHE_KEY hashes the recipe-relevant content of prepare.py (comment-only
# and blank lines dropped, so formatting churn does not rotate the key) plus
# win.bat and the SDK. Per-library cache_keys inside prepare.py re-validate
# every stage regardless, so CACHE_KEY only affects which tarball we select.
{ grep -vE '^[[:space:]]*#' "$TBUILD/$REPO_NAME/$PREPARE_PATH" || true; } | sed '/^[[:space:]]*$/d' | sha256sum | awk '{ print $1 }' >> CACHE_KEY.txt
sha256sum "$TBUILD/$REPO_NAME/$(dirname "$PREPARE_PATH")/win.bat" | awk '{ print $1 }' >> CACHE_KEY.txt
echo "$SDK" >> CACHE_KEY.txt
CACHE_KEY_VALUE="$(sha256sum CACHE_KEY.txt | awk '{ print $1 }')"
echo "CACHE_KEY=$CACHE_KEY_VALUE" >> $GITHUB_ENV
TELEGRAM_BUILD_CACHE_SCOPE="${{ runner.OS }}-${{ matrix.arch }}-telegram-build-${TELEGRAM_BUILD_CACHE_VERSION}-${SDK}-${{ matrix.qt || 'qt' }}-${{ matrix.generator || 'ninja-multi-config' }}-${{ matrix.defines || 'default' }}-${{ (github.ref == 'refs/heads/nightly' || startsWith(github.ref, 'refs/tags/')) && 'lto' || 'nolto' }}-${CACHE_KEY_VALUE}"
{
printf '%s\n' "$TELEGRAM_BUILD_CACHE_SCOPE"
git -C "$TBUILD/$REPO_NAME" rev-parse HEAD
git -C "$TBUILD/$REPO_NAME" submodule status --recursive
sha256sum "$TBUILD/$REPO_NAME/.github/workflows/win.yml"
sha256sum "$TBUILD/$REPO_NAME/CMakeLists.txt"
sha256sum "$TBUILD/$REPO_NAME/Telegram/CMakeLists.txt"
sha256sum "$TBUILD/$REPO_NAME/Telegram/build/prepare/win.bat"
} > TELEGRAM_BUILD_CACHE_KEY.txt
TELEGRAM_BUILD_CACHE_KEY="$(sha256sum TELEGRAM_BUILD_CACHE_KEY.txt | awk '{ print $1 }')"
TELEGRAM_BUILD_CACHE_RESTORE_KEY="$TELEGRAM_BUILD_CACHE_SCOPE-$TELEGRAM_BUILD_CACHE_KEY"
TELEGRAM_BUILD_CACHE_LOOKUP_KEY="${TELEGRAM_BUILD_CACHE_RESTORE_KEY}-lookup-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
TELEGRAM_BUILD_CACHE_PROGRESS_KEY="${TELEGRAM_BUILD_CACHE_RESTORE_KEY}-progress-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
echo "TELEGRAM_BUILD_CACHE_SCOPE=$TELEGRAM_BUILD_CACHE_SCOPE" >> $GITHUB_ENV
echo "TELEGRAM_BUILD_CACHE_KEY=$TELEGRAM_BUILD_CACHE_KEY" >> $GITHUB_ENV
echo "TELEGRAM_BUILD_CACHE_RESTORE_KEY=$TELEGRAM_BUILD_CACHE_RESTORE_KEY" >> $GITHUB_ENV
echo "TELEGRAM_BUILD_CACHE_LOOKUP_KEY=$TELEGRAM_BUILD_CACHE_LOOKUP_KEY" >> $GITHUB_ENV
echo "TELEGRAM_BUILD_CACHE_PROGRESS_KEY=$TELEGRAM_BUILD_CACHE_PROGRESS_KEY" >> $GITHUB_ENV
echo "Configurate git for cherry-picks."
git config --global user.email "you@example.com"
git config --global user.name "Sample"
- name: Proxy control-plane source guards.
shell: bash
run: |
cd "$TBUILD/$REPO_NAME"
python Telegram/SourceFiles/tests/test_mtproto_relay_silence.py
python Telegram/SourceFiles/tests/test_mtproxy_client_hello_profiles.py
python Telegram/SourceFiles/tests/test_mtproxy_endpoint_identity.py
python Telegram/SourceFiles/tests/test_mtproxy_faketls_hardening.py
python Telegram/SourceFiles/tests/test_mtproxy_minimal_hotfix.py
python Telegram/SourceFiles/tests/test_mtproxy_phase_cooldown.py
python Telegram/SourceFiles/tests/test_mtproxy_policy.py
python Telegram/SourceFiles/tests/test_mtproxy_target_flow.py
python Telegram/SourceFiles/tests/test_mtproxy_tls_psk.py
python Telegram/SourceFiles/tests/test_proxy_atomic_switch.py
python Telegram/SourceFiles/tests/test_proxy_capability_cache.py
python Telegram/SourceFiles/tests/test_proxy_capability_key_contract.py
python Telegram/SourceFiles/tests/test_proxy_connection_status.py
python Telegram/SourceFiles/tests/test_proxy_control_plane.py
python Telegram/SourceFiles/tests/test_proxy_control_plane_truth_table.py
python Telegram/SourceFiles/tests/test_proxy_diagnostics.py
python Telegram/SourceFiles/tests/test_proxy_list_checks.py
python Telegram/SourceFiles/tests/test_proxy_logging_events.py
python Telegram/SourceFiles/tests/test_proxy_shield_probe.py
python Telegram/SourceFiles/tests/test_proxy_wss_default.py
python Telegram/SourceFiles/tests/test_session_endpoint_cooldown.py
- name: Install VS 2026 Build Tools.
if: matrix.arch == 'arm64'
shell: pwsh
run: |
Invoke-WebRequest -Uri https://aka.ms/vs/18/stable/vs_buildtools.exe -OutFile vs_buildtools.exe
$p = Start-Process -FilePath .\vs_buildtools.exe -Wait -PassThru -ArgumentList `
'--quiet','--wait','--norestart','--nocache', `
'--add','Microsoft.VisualStudio.Workload.VCTools', `
'--add','Microsoft.VisualStudio.Component.VC.Tools.ARM64', `
'--add','Microsoft.VisualStudio.Component.VC.ATL.ARM64'
Remove-Item vs_buildtools.exe -Force
if ($p.ExitCode -ne 0 -and $p.ExitCode -ne 3010) { exit $p.ExitCode }
- uses: Eden-CI/msvc-dev-cmd@master
name: Native Tools Command Prompt.
with:
arch: ${{ matrix.arch }}
sdk: ${{ env.SDK }}
toolset: ${{ case(matrix.arch == 'arm64', '', '14.44') }}
- name: NuGet sources.
run: |
nuget sources Disable -Name "Microsoft Visual Studio Offline Packages"
nuget sources Add -Source https://api.nuget.org/v3/index.json & exit 0
- name: ThirdParty cache (restore).
id: cache-third-party
uses: actions/cache/restore@v5
with:
path: ${{ env.TBUILD }}\ThirdParty
key: ${{ runner.OS }}-${{ runner.arch }}-third-party-${{ env.CACHE_VERSION }}-${{ env.SDK }}-${{ env.CACHE_KEY }}
restore-keys: |
${{ runner.OS }}-${{ runner.arch }}-third-party-${{ env.CACHE_VERSION }}-${{ env.SDK }}-
${{ runner.OS }}-${{ runner.arch }}-third-party-${{ env.CACHE_VERSION }}-
${{ runner.OS }}-${{ runner.arch }}-third-party-
- name: Libraries cache (restore).
id: cache-libs
uses: actions/cache/restore@v5
with:
path: |
${{ env.LibrariesPath }}\*
!${{ env.LibrariesPath }}\cache_keys
!${{ env.LibrariesPath }}\[qQ]t[_-]*
${{ env.LibrariesPath }}\cache_keys\*
!${{ env.LibrariesPath }}\cache_keys\[qQ]t[_-]*
key: ${{ runner.OS }}-${{ matrix.arch }}-libs-${{ env.LIBS_CACHE_VERSION }}-${{ env.SDK }}-${{ env.CACHE_KEY }}
restore-keys: |
${{ runner.OS }}-${{ matrix.arch }}-libs-${{ env.LIBS_CACHE_VERSION }}-${{ env.SDK }}-
${{ runner.OS }}-${{ matrix.arch }}-libs-${{ env.LIBS_CACHE_VERSION }}-
- name: Qt cache (restore).
id: cache-qt
uses: actions/cache/restore@v5
with:
path: |
${{ env.LibrariesPath }}\[qQ]t[_-]*
${{ env.LibrariesPath }}\cache_keys\[qQ]t[_-]*
key: ${{ runner.OS }}-${{ matrix.arch }}-${{ matrix.qt || 'qt' }}-${{ env.CACHE_VERSION }}-${{ env.SDK }}-${{ env.CACHE_KEY }}
restore-keys: |
${{ runner.OS }}-${{ matrix.arch }}-${{ matrix.qt || 'qt' }}-${{ env.CACHE_VERSION }}-${{ env.SDK }}-
${{ runner.OS }}-${{ matrix.arch }}-${{ matrix.qt || 'qt' }}-${{ env.CACHE_VERSION }}-
${{ runner.OS }}-${{ matrix.arch }}-${{ matrix.qt || 'qt' }}-
# Self-healing sanity check on the restored caches, before win.bat trusts them.
# prepare.py marks a stage 'Good' if <dir>/<name> exists and cache_keys/<name>
# matches — it never looks INSIDE the dir, so a truncated restore (zero-byte
# .lib) or a dir emptied by the trim filter would be silently reused. Deleting
# a per-stage key only forces THAT stage to rebuild (always safe), so we err
# toward deleting on any corruption signal. Never fails the job.
- name: Verify restored caches.
shell: bash
run: |
shopt -s nullglob
check_tree() {
local root="$1"
[ -d "$root/cache_keys" ] || return 0
local key name dir
for key in "$root"/cache_keys/*; do
name="$(basename "$key")"
dir="$root/$name"
if [ ! -d "$dir" ]; then
rm -f "$key"; echo "smoke: $name -> orphan key (no artifact dir), will rebuild"; continue
fi
if [ -n "$(find "$dir" -type f \( -name '*.lib' -o -name '*.a' -o -name '*.exe' \) -size 0 -print -quit)" ]; then
rm -f "$key"; echo "smoke: $name -> zero-byte artifact (truncated restore), will rebuild"; continue
fi
done
}
# The trim filter keeps build products only, so a stage that a LATER
# stage consumes as sources comes back from the cache stripped of the
# files that stage needs: ffmpeg's build_ffmpeg_win.sh runs `make
# install` inside nv-codec-headers, and its Makefile is not a build
# product. The dir survives (its headers are kept), so the generic
# checks above see nothing wrong. Drop the key when the probe file is
# missing — prepare.py wipes and re-clones the stage, which is cheap.
check_sources() {
local root="$1" name="$2" probe="$3"
[ -d "$root/$name" ] || return 0
[ -f "$root/$name/$probe" ] && return 0
rm -f "$root/cache_keys/$name"
echo "smoke: $name -> sources trimmed ($probe missing), will rebuild"
}
check_tree "${LibrariesPath//\\//}"
check_tree "${TBUILD//\\//}/ThirdParty"
check_sources "${LibrariesPath//\\//}" nv-codec-headers Makefile
echo "smoke: cache verification done"
- name: Libraries.
id: build-libs
run: |
%TBUILD%\%REPO_NAME%\Telegram\build\prepare\win.bat skip-debug silent ${{ matrix.qt }}
- name: sccache.
if: env.ONLY_CACHE == 'false'
uses: mozilla-actions/sccache-action@v0.0.10
with:
version: "v0.16.0"
disable_annotations: "true"
- name: Start sccache server.
if: env.ONLY_CACHE == 'false'
shell: bash
run: |
${SCCACHE_PATH:-sccache} --start-server
${SCCACHE_PATH:-sccache} --zero-stats
- name: Read configuration matrix.
shell: bash
run: |
ARTIFACT_NAME="Telegram"
ARCH=""
if [ -n "${{ matrix.arch }}" ]; then
case "${{ matrix.arch }}" in
x64_x86) ARCH="x86";;
arm64) ARCH="arm";;
*) ARCH="${{ matrix.arch }}";;
esac
echo "Architecture from matrix: $ARCH"
ARTIFACT_NAME="${ARTIFACT_NAME} ${{ matrix.arch }}"
fi
if [ -n "${{ matrix.qt }}" ]; then
ARTIFACT_NAME="${ARTIFACT_NAME} ${{ matrix.qt }}"
fi
GENERATOR="-G \"Ninja Multi-Config\""
echo "Generator: $GENERATOR"
if [ -n "${{ matrix.generator }}" ]; then
GENERATOR="-G \"${{ matrix.generator }}\""
echo "Generator from matrix: $GENERATOR"
ARTIFACT_NAME="${ARTIFACT_NAME} ${{ matrix.generator }}"
fi
echo "TDESKTOP_BUILD_GENERATOR=$GENERATOR" >> $GITHUB_ENV
[ -n "$GENERATOR" ] && ARCH=""
echo "TDESKTOP_BUILD_ARCH=$ARCH" >> $GITHUB_ENV
DEFINE=""
if [ -n "${{ matrix.defines }}" ]; then
DEFINE="-D ${{ matrix.defines }}=ON"
echo "Define from matrix: $DEFINE"
ARTIFACT_NAME="${ARTIFACT_NAME} ${{ matrix.defines }}"
fi
echo "TDESKTOP_BUILD_DEFINE=$DEFINE" >> $GITHUB_ENV
echo "ARTIFACT_NAME=$ARTIFACT_NAME" >> $GITHUB_ENV
API="-D TDESKTOP_API_TEST=ON"
if [ $GITHUB_REF == 'refs/heads/nightly' ]; then
echo "Use the open credentials."
API="-D TDESKTOP_API_ID=611335 -D TDESKTOP_API_HASH=d524b414d21f4d37f08684c1df41ac9c"
fi
echo "TDESKTOP_BUILD_API=$API" >> $GITHUB_ENV
# Keeps build products only — plus the few inputs later stages read back out
# of the tree: *.pc (ffmpeg's configure resolves dav1d/opus/... through
# pkg-config) and Makefile (build_ffmpeg_win.sh runs `make install` inside
# nv-codec-headers). Dropping those makes any ffmpeg rebuild fail against an
# otherwise valid cache. Bump LIBS_CACHE_VERSION when this filter changes.
- name: Free up some disk space.
shell: bash
run: find $LibrariesPath '(' '(' ! '(' -name '*.lib' -o -name '*.a' -o -name '*.exe' -o -name '*.h' -o -name '*.hpp' -o -name '*.inc' -o -name '*.cmake' -o -name '*.pc' -o -name 'Makefile' -o -path '*/include/*' -o -path '*/objects-*' -o -path '*/cache_keys/*' -o -path '*/patches/*' ')' -type f ')' -o -empty ')' -delete
# Save caches BEFORE the (possibly failing) Telegram compile, gated on the
# libraries step having succeeded — not on the whole job. This is the fix for
# "compile fails -> nothing cached -> libraries rebuilt every run".
# !cancelled() -> save on compile failure, skip on cancellation
# build-libs.outcome == 'success' -> never cache a half-built libraries tree
# cache-hit != 'true' -> don't re-save an already-present exact key
- name: ThirdParty cache (save).
if: >-
!cancelled()
&& steps.build-libs.outcome == 'success'
&& steps.cache-third-party.outputs.cache-hit != 'true'
continue-on-error: true
uses: actions/cache/save@v5
with:
path: ${{ env.TBUILD }}\ThirdParty
key: ${{ steps.cache-third-party.outputs.cache-primary-key }}
- name: Libraries cache (save).
if: >-
!cancelled()
&& steps.build-libs.outcome == 'success'
&& steps.cache-libs.outputs.cache-hit != 'true'
continue-on-error: true
uses: actions/cache/save@v5
with:
path: |
${{ env.LibrariesPath }}\*
!${{ env.LibrariesPath }}\cache_keys
!${{ env.LibrariesPath }}\[qQ]t[_-]*
${{ env.LibrariesPath }}\cache_keys\*
!${{ env.LibrariesPath }}\cache_keys\[qQ]t[_-]*
key: ${{ steps.cache-libs.outputs.cache-primary-key }}
- name: Qt cache (save).
if: >-
!cancelled()
&& steps.build-libs.outcome == 'success'
&& steps.cache-qt.outputs.cache-hit != 'true'
continue-on-error: true
uses: actions/cache/save@v5
with:
path: |
${{ env.LibrariesPath }}\[qQ]t[_-]*
${{ env.LibrariesPath }}\cache_keys\[qQ]t[_-]*
key: ${{ steps.cache-qt.outputs.cache-primary-key }}
- name: Telegram build cache (restore).
if: env.ONLY_CACHE == 'false'
id: cache-telegram-build
uses: actions/cache/restore@v5
with:
path: |
${{ env.TBUILD }}\${{ env.REPO_NAME }}\out
!${{ env.TBUILD }}\${{ env.REPO_NAME }}\out\Release\Telegram.exe
!${{ env.TBUILD }}\${{ env.REPO_NAME }}\out\Release\Telegram.pdb
!${{ env.TBUILD }}\${{ env.REPO_NAME }}\out\Telegram\Release\Telegram.lib
!${{ env.TBUILD }}\${{ env.REPO_NAME }}\out\Telegram\Release\Telegram.exp
key: ${{ env.TELEGRAM_BUILD_CACHE_LOOKUP_KEY }}
restore-keys: |
${{ env.TELEGRAM_BUILD_CACHE_RESTORE_KEY }}-progress-
${{ env.TELEGRAM_BUILD_CACHE_RESTORE_KEY }}
${{ env.TELEGRAM_BUILD_CACHE_SCOPE }}-
- name: Normalize Telegram source mtimes.
if: env.ONLY_CACHE == 'false' && steps.cache-telegram-build.outputs.cache-matched-key != ''
shell: bash
env:
TELEGRAM_BUILD_CACHE_HIT: ${{ steps.cache-telegram-build.outputs.cache-hit }}
TELEGRAM_BUILD_CACHE_MATCHED_KEY: ${{ steps.cache-telegram-build.outputs.cache-matched-key }}
run: |
cd "${TBUILD//\\//}/${REPO_NAME}"
python - <<'PY'
import os
import subprocess
from pathlib import Path
repo = Path.cwd()
metadata = repo / "out" / ".telegram_build_cache_metadata"
old_mtime = 946684800
def run_git(args, cwd=repo, check=True):
result = subprocess.run(
["git", *args],
cwd=cwd,
check=check,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL)
return result.stdout
def valid_commit(value):
if not value or set(value) == {"0"}:
return False
return subprocess.run(
["git", "cat-file", "-e", f"{value}^{{commit}}"],
cwd=repo,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL).returncode == 0
def ensure_commit(value):
if not value or set(value) == {"0"}:
return False
if valid_commit(value):
return True
subprocess.run(
["git", "fetch", "--no-tags", "--depth=1", "origin", value],
cwd=repo,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
return valid_commit(value)
cached_head = ""
if metadata.exists():
for line in metadata.read_text(encoding="utf-8").splitlines():
if line.startswith("head="):
cached_head = line.split("=", 1)[1].strip()
break
if (
not valid_commit(cached_head)
and os.environ.get("TELEGRAM_BUILD_CACHE_HIT") == "true"
):
cached_head = run_git(["rev-parse", "HEAD"]).decode().strip()
legacy_cache_heads = {
"97e7512e600590ca2254c7bd523c2b07346b494f8c07d2334e9fff922a2c7e73":
"dc7fc515605489f2486904c1a2d3e60811335b54",
"e48a776cea3c96cdf4cfbf0bcd71426b2945ba854f733ac6c1774413f89253bb":
"dc7fc515605489f2486904c1a2d3e60811335b54",
}
matched_key = os.environ.get("TELEGRAM_BUILD_CACHE_MATCHED_KEY", "")
if not ensure_commit(cached_head):
for suffix, head in legacy_cache_heads.items():
if matched_key.endswith(suffix):
cached_head = head
break
if not ensure_commit(cached_head):
print("mtime: no comparable cached head, leaving checkout mtimes")
raise SystemExit(0)
changed = set(filter(None, run_git([
"diff",
"--name-only",
"-z",
cached_head,
"HEAD",
"--",
]).decode("utf-8", "surrogateescape").split("\0")))
cmake_graph_inputs = (
"CMakeLists.txt",
"Telegram/CMakeLists.txt",
"Telegram/cmake/",
"Telegram/configure.",
"cmake/",
)
cmake_graph_changed = any(
item.endswith("CMakeLists.txt")
or item.endswith(".cmake")
or item.startswith(cmake_graph_inputs)
for item in changed)
def changed_path(path):
path = path.as_posix()
return path in changed or any(
item.startswith(path + "/") for item in changed)
def normalize_file(path):
try:
os.utime(path, (old_mtime, old_mtime))
except FileNotFoundError:
pass
for name in filter(None, run_git(["ls-files", "-z"]).split(b"\0")):
rel = Path(os.fsdecode(name))
if not changed_path(rel):
normalize_file(repo / rel)
submodule_paths = []
config = run_git(
["config", "--file", ".gitmodules", "--get-regexp", "path"],
check=False).decode("utf-8", "replace")
for line in config.splitlines():
parts = line.split(maxsplit=1)
if len(parts) == 2:
submodule_paths.append(Path(parts[1]))
for rel_root in submodule_paths:
if changed_path(rel_root):
continue
root = repo / rel_root
if not root.exists():
continue
files = run_git(["ls-files", "-z"], cwd=root, check=False)
for name in filter(None, files.split(b"\0")):
normalize_file(root / Path(os.fsdecode(name)))
print(
f"mtime: normalized unchanged sources from {cached_head[:12]} "
f"to keep restored Ninja outputs reusable")
cache = repo / "out" / "CMakeCache.txt"
if cache.exists() and not cmake_graph_changed:
with open(os.environ["GITHUB_ENV"], "a", encoding="utf-8") as env:
env.write("TELEGRAM_SKIP_CONFIGURE=true\n")
print("mtime: restored CMake configure can be reused")
elif cmake_graph_changed:
print("mtime: CMake graph changed, configure will run")
PY
- name: Bake ZaStoGram build id.
if: env.ONLY_CACHE == 'false'
shell: bash
run: |
# Must run after the mtime normalization: that step rewinds the mtime
# of every unchanged tracked file, so baking earlier would let Ninja
# keep the object compiled with the previous build id.
cd "${TBUILD//\\//}/${REPO_NAME}"
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
# A tagged release is fully identified by its version number.
BUILD_ID=""
else
BUILD_ID="dev-${GITHUB_RUN_NUMBER}"
fi
HEADER=Telegram/SourceFiles/core/zsg_build.h
sed -i "s|^constexpr auto ZsgBuildId = .*|constexpr auto ZsgBuildId = \"$BUILD_ID\"_cs;|" "$HEADER"
if ! grep -qF "constexpr auto ZsgBuildId = \"$BUILD_ID\"_cs;" "$HEADER"; then
echo "Failed to bake the build id into $HEADER!"
exit 1
fi
tail -n 1 "$HEADER"
- name: Prepare update signing keys.
if: env.ONLY_CACHE == 'false'
shell: bash
env:
PACKER_PRIVATE_H: ${{ secrets.PACKER_PRIVATE_H }}
run: |
mkdir -p "$TBUILD/DesktopPrivate"
if [ -n "$PACKER_PRIVATE_H" ]; then
printf '%s' "$PACKER_PRIVATE_H" > "$TBUILD/DesktopPrivate/packer_private.h"
echo "Using real update signing key from secrets."
else
{
echo '// Placeholder: real keys come from the PACKER_PRIVATE_H secret.'
echo 'const char *PrivateKey = "";'
echo 'const char *PrivateBetaKey = "";'
} > "$TBUILD/DesktopPrivate/packer_private.h"
echo "PACKER_PRIVATE_H secret is not set, Packer will not be able to sign."
fi
echo 'static const char *AlphaPrivateKey = "";' > "$TBUILD/DesktopPrivate/alpha_private.h"
- name: Telegram Desktop build.
id: build-telegram
if: env.ONLY_CACHE == 'false'
run: |
cd %TBUILD%\%REPO_NAME%\Telegram
if "%TELEGRAM_SKIP_CONFIGURE%"=="true" (
echo Reusing restored CMake configure.
) else (
call configure.bat ^
%TDESKTOP_BUILD_GENERATOR% ^
%TDESKTOP_BUILD_ARCH% ^
${{ matrix.qt }} ^
%TDESKTOP_BUILD_API% ^
-D CMAKE_CONFIGURATION_TYPES=Release ^
-D DESKTOP_APP_ENABLE_LTO=${{ (github.ref == 'refs/heads/nightly' || startsWith(github.ref, 'refs/tags/')) && 'ON' || 'OFF' }} ^
-D CMAKE_C_COMPILER_LAUNCHER=sccache ^
-D CMAKE_CXX_COMPILER_LAUNCHER=sccache ^
-D CMAKE_COMPILE_WARNING_AS_ERROR=ON ^
-D CMAKE_MSVC_DEBUG_INFORMATION_FORMAT= ^
-D DESKTOP_APP_DISABLE_AUTOUPDATE=OFF ^
-D ZASTOGRAM_PACKER=ON ^
-D DESKTOP_APP_DISABLE_QT_PLUGINS=ON ^
-D DESKTOP_APP_DISABLE_CRASH_REPORTS=OFF ^
%TDESKTOP_BUILD_DEFINE%
)
cmake --build ..\out --config Release --target Telegram Packer --parallel
- name: Telegram build cache metadata.
if: >-
always()
&& !cancelled()
&& env.ONLY_CACHE == 'false'
&& steps.build-telegram.outcome != 'skipped'
shell: bash
run: |
cd "${TBUILD//\\//}/${REPO_NAME}"
mkdir -p out
# A stopped build leaves .obj files that still match the RESTORED
# cache's head for every source it never got to recompile. Advancing
# head= past them would make the next run's mtime normalization mark
# those stale objects as up to date forever (unresolved externals at
# link time), so on failure keep the restored metadata as is.
if [ "${{ steps.build-telegram.outcome }}" != "success" ] \
&& [ -f out/.telegram_build_cache_metadata ]; then
echo "Build failed, keeping restored cache metadata."
exit 0
fi
{
printf 'head=%s\n' "$(git rev-parse HEAD)"
printf 'submodules\n'
git submodule status --recursive || true
} > out/.telegram_build_cache_metadata
# GitHub cache entries are immutable. Successful builds save the stable
# primary key; failed builds save a unique progress key, restored first
# on the next run so late compile failures keep the completed .obj files.
- name: Telegram build cache (save).
id: save-telegram-build
if: >-
always()
&& !cancelled()
&& env.ONLY_CACHE == 'false'
&& steps.build-telegram.outcome != 'skipped'
&& (
steps.build-telegram.outcome == 'failure'
|| steps.cache-telegram-build.outputs.cache-matched-key != env.TELEGRAM_BUILD_CACHE_RESTORE_KEY
)
continue-on-error: true
uses: actions/cache/save@v5
with:
path: |
${{ env.TBUILD }}\${{ env.REPO_NAME }}\out
!${{ env.TBUILD }}\${{ env.REPO_NAME }}\out\Release\Telegram.exe
!${{ env.TBUILD }}\${{ env.REPO_NAME }}\out\Release\Telegram.pdb
!${{ env.TBUILD }}\${{ env.REPO_NAME }}\out\Telegram\Release\Telegram.lib
!${{ env.TBUILD }}\${{ env.REPO_NAME }}\out\Telegram\Release\Telegram.exp
key: ${{ steps.build-telegram.outcome == 'failure' && env.TELEGRAM_BUILD_CACHE_PROGRESS_KEY || env.TELEGRAM_BUILD_CACHE_RESTORE_KEY }}
# Cache entries are immutable and the repo budget is 10 GB. Every push
# saves a fresh ~1.1-1.3 GB out/ snapshot per arch, so superseded
# generations would push the repo over budget within a day and LRU
# eviction would silently drop sccache objects and even the freshest
# build caches (observed: a run finding NO telegram-build cache at all).
# After a successful save keep, per arch, only:
# - the entry just saved (or the exact-key entry we restored from),
# - entries of the sibling lto/nolto scope with the SAME recipe hash
# (those are pruned by their own lto/nolto runs).
# Older generations, rotated scopes and -progress- leftovers are deleted
# here instead of waiting for the age-based nightly cleanup. Runs only on
# build success: after a failure the restored-from entry must survive.
- name: Prune superseded build caches.
if: >-
always()
&& !cancelled()
&& env.ONLY_CACHE == 'false'
&& steps.build-telegram.outcome == 'success'
&& (
steps.save-telegram-build.outcome == 'success'
|| steps.cache-telegram-build.outputs.cache-matched-key == env.TELEGRAM_BUILD_CACHE_RESTORE_KEY
)
continue-on-error: true
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
PREFIX="${{ runner.OS }}-${{ matrix.arch }}-telegram-build-"
case "$TELEGRAM_BUILD_CACHE_SCOPE" in
*-nolto-*) SIBLING="${TELEGRAM_BUILD_CACHE_SCOPE/-nolto-/-lto-}" ;;
*) SIBLING="${TELEGRAM_BUILD_CACHE_SCOPE/-lto-/-nolto-}" ;;
esac
if ! rows=$(gh cache list -R "$GITHUB_REPOSITORY" --key "$PREFIX" \
--limit 200 --json id,key --jq '.[] | "\(.id)\t\(.key)"'); then
echo "warn: gh cache list failed, skipping prune" >&2
exit 0
fi
deleted=0
while IFS=$'\t' read -r id key; do
[ -n "$id" ] || continue
[ "$key" = "$TELEGRAM_BUILD_CACHE_RESTORE_KEY" ] && continue
case "$key" in
"$SIBLING"*) continue ;;
esac
if gh cache delete -R "$GITHUB_REPOSITORY" "$id"; then
echo "pruned: $key"
deleted=$((deleted+1))
else
echo "warn: failed to delete cache $id ($key)" >&2
fi
done <<< "$rows"
echo "Pruned $deleted superseded cache entrie(s)."
- name: sccache stats.
if: always() && env.ONLY_CACHE == 'false'
shell: bash
run: |
if ! command -v "${SCCACHE_PATH:-sccache}" >/dev/null 2>&1; then
echo "sccache not found, skipping stats."
exit 0
fi
${SCCACHE_PATH:-sccache} --show-stats || true
- name: sccache error log.
if: always() && env.ONLY_CACHE == 'false'
shell: bash
run: cat "${SCCACHE_ERROR_LOG//\\//}" 2>/dev/null || echo "(no sccache error log)"
- name: Stop sccache server.
if: always() && env.ONLY_CACHE == 'false'
shell: bash
run: |
if ! command -v "${SCCACHE_PATH:-sccache}" >/dev/null 2>&1; then
echo "sccache not found, skipping stop."
exit 0
fi
${SCCACHE_PATH:-sccache} --stop-server || true
- name: Pack signed update.
if: env.ONLY_CACHE == 'false' && startsWith(github.ref, 'refs/tags/')
shell: bash
run: |
cd "$TBUILD/$REPO_NAME/out/Release"
VERSION=$(awk '$1 == "AppVersion" { print $2 }' "$TBUILD/$REPO_NAME/Telegram/build/version")
case "${{ matrix.arch }}" in
x64) TARGET="-target win64" ;;
arm64) TARGET="-target winarm" ;;
*) TARGET="" ;;
esac
./Packer.exe -path Telegram.exe -path Updater.exe -version "$VERSION" $TARGET
ls -la t*upd*
- name: Move artifact.
if: (env.UPLOAD_ARTIFACT == 'true') || (github.ref == 'refs/heads/nightly')
run: |
set OUT=%TBUILD%\%REPO_NAME%\out\Release
mkdir artifact
move %OUT%\Telegram.exe artifact/
move %OUT%\Updater.exe artifact/
if exist %OUT%\tupdate* move %OUT%\tupdate* artifact/
if exist %OUT%\tx64upd* move %OUT%\tx64upd* artifact/
if exist %OUT%\tarm64upd* move %OUT%\tarm64upd* artifact/
- uses: actions/upload-artifact@v7
name: Upload artifact.
if: (env.UPLOAD_ARTIFACT == 'true') || (github.ref == 'refs/heads/nightly')
with:
name: ${{ env.ARTIFACT_NAME }}
path: artifact\
release:
name: Pre-release
needs: windows
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download artifacts.
uses: actions/download-artifact@v7
with:
path: artifacts
- name: Collect binaries.
run: |
mkdir release
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
release_id="$(printf '%s' "$GITHUB_REF_NAME" | sed 's#[/\\:*?"<>|]#-#g')"
else
release_id="dev-${GITHUB_RUN_NUMBER}"
fi
for d in artifacts/*/; do
name="$(basename "$d")"
arch="$(echo "$name" | sed 's/^Telegram //; s/ qt6//; s/ /_/g')"
if [ -f "$d/Telegram.exe" ]; then
branded_exe="$GITHUB_WORKSPACE/release/ZaStoGram-$arch.exe"
cp "$d/Telegram.exe" "$branded_exe"
zip -j -9 "$GITHUB_WORKSPACE/release/ZaStoGram-$release_id-$arch.zip" "$branded_exe"
fi
if [ -f "$d/Telegram.exe" ] && [ -f "$d/Updater.exe" ]; then
(cd "$d" && zip -j "$GITHUB_WORKSPACE/release/ZaStoGram-portable-$arch.zip" Telegram.exe Updater.exe)
fi
cp "$d"/tupdate* "$d"/tx64upd* "$d"/tarm64upd* release/ 2>/dev/null || true
done
ls -la release
- name: Generate update map (current4).
if: startsWith(github.ref, 'refs/tags/')
run: |
ver=""
for f in release/tupdate* release/tx64upd* release/tarm64upd*; do
[ -e "$f" ] || continue
ver="$(basename "$f" | grep -oE '[0-9]+$')"
done
if [ -z "$ver" ]; then
echo "No signed update packages found, cannot generate current4!"
exit 1
fi
current='{}'
add() {
current="$(echo "$current" | jq --arg k "$1" --arg v "$ver" --arg l "$2" \
'. + {($k): {stable: {released: $v, link: $l}}}')"
}
[ -e "release/tupdate$ver" ] && add win "/tupdate{version}"
[ -e "release/tx64upd$ver" ] && add win64 "/tx64upd{version}"
[ -e "release/tarm64upd$ver" ] && add winarm "/tarm64upd{version}"
echo "$current" | tee release/current4
- name: Publish release.
env:
GH_TOKEN: ${{ github.token }}
run: |
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
gh release create "${{ github.ref_name }}" release/* \
--repo "${{ github.repository }}" \
--title "ZaStoGram ${{ github.ref_name }}" \
--generate-notes \
--latest
else
gh release create "dev-${{ github.run_number }}" release/* \
--repo "${{ github.repository }}" \
--target "${{ github.sha }}" \
--title "ZaStoGram dev ${{ github.run_number }}" \
--notes "Automated pre-release from ${{ github.sha }}." \
--prerelease
fi