Skip to content

Commit 4f37f46

Browse files
committed
Added monthly IWYU cleanup workflow.
1 parent 4e10ec4 commit 4f37f46

4 files changed

Lines changed: 372 additions & 5 deletions

File tree

.github/workflows/iwyu.yml

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
name: IWYU.
2+
3+
on:
4+
schedule:
5+
- cron: '0 3 1 * *'
6+
workflow_dispatch:
7+
inputs:
8+
ref:
9+
description: 'Branch to clean up.'
10+
type: string
11+
default: dev
12+
push:
13+
branches: [iwyu-test]
14+
15+
permissions:
16+
contents: write
17+
pull-requests: write
18+
19+
jobs:
20+
21+
generate:
22+
name: Generate patch
23+
runs-on: depot-ubuntu-latest-16
24+
outputs:
25+
has_changes: ${{ steps.diff.outputs.has_changes }}
26+
ref: ${{ steps.target.outputs.ref }}
27+
env:
28+
IMAGE_TAG: tdesktop:centos_env
29+
30+
steps:
31+
- name: Resolve target ref.
32+
id: target
33+
run: echo "ref=${{ inputs.ref || (github.event_name == 'push' && github.ref_name) || 'dev' }}" >> $GITHUB_OUTPUT
34+
35+
- name: Clone.
36+
uses: actions/checkout@v6
37+
with:
38+
ref: ${{ steps.target.outputs.ref }}
39+
submodules: recursive
40+
41+
- name: First set up.
42+
run: |
43+
sudo apt update
44+
curl -sSL https://install.python-poetry.org | python3 -
45+
cd Telegram/build/docker/centos_env
46+
poetry install
47+
DOCKERFILE=$(DEBUG= LTO= poetry run gen_dockerfile)
48+
echo "$DOCKERFILE" > Dockerfile
49+
rm -rf __pycache__
50+
51+
- name: Free up some disk space.
52+
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be
53+
with:
54+
tool-cache: true
55+
56+
- name: Set up Docker Buildx.
57+
uses: docker/setup-buildx-action@v4
58+
59+
- name: Libraries cache.
60+
uses: actions/cache@v5
61+
with:
62+
path: ${{ runner.temp }}/.buildx-cache
63+
key: ${{ runner.OS }}-libs-${{ hashFiles('Telegram/build/docker/centos_env/**') }}
64+
restore-keys: ${{ runner.OS }}-libs-
65+
66+
- name: Libraries.
67+
uses: docker/build-push-action@v7
68+
with:
69+
context: Telegram/build/docker/centos_env
70+
load: true
71+
tags: ${{ env.IMAGE_TAG }}
72+
cache-from: type=local,src=${{ runner.temp }}/.buildx-cache
73+
cache-to: type=local,dest=${{ runner.temp }}/.buildx-cache-new,mode=max
74+
75+
- name: Move cache.
76+
run: |
77+
rm -rf ${{ runner.temp }}/.buildx-cache
78+
mv ${{ runner.temp }}/.buildx-cache{-new,}
79+
80+
- name: Configure & build (codegen + compile_commands).
81+
run: |
82+
docker run --rm \
83+
-u $(id -u) \
84+
-v $PWD:/usr/src/tdesktop \
85+
-e CONFIG=Debug \
86+
$IMAGE_TAG \
87+
/usr/src/tdesktop/Telegram/build/docker/centos_env/build.sh \
88+
-D CMAKE_CONFIGURATION_TYPES=Debug \
89+
-D CMAKE_C_FLAGS_DEBUG="-O0 -fuse-ld=lld" \
90+
-D CMAKE_CXX_FLAGS_DEBUG="-O0 -fuse-ld=lld" \
91+
-D CMAKE_EXPORT_COMPILE_COMMANDS=ON \
92+
-D TDESKTOP_API_TEST=ON \
93+
-D DESKTOP_APP_DISABLE_AUTOUPDATE=OFF \
94+
-D DESKTOP_APP_DISABLE_CRASH_REPORTS=OFF
95+
96+
- name: Strip GCC-only flags from compile_commands.json.
97+
# Clang (used by IWYU) does not know -fhardened; it aborts the parse
98+
# of the command line before any analysis runs.
99+
run: sed -i 's/ -fhardened//g' out/compile_commands.json
100+
101+
- name: Run include-what-you-use.
102+
# The script body is materialised via a quoted heredoc so that any
103+
# special character in comments or strings (apostrophes, quotes,
104+
# backslashes) cannot break the outer `bash -ec '...'` quoting and
105+
# silently spill commands onto the host runner.
106+
run: |
107+
cat > iwyu.sh <<'SCRIPT'
108+
jq -r ".[].file | select(test(\"Telegram/SourceFiles/\"))" out/compile_commands.json > iwyu_files.txt
109+
echo "Analyzing $(wc -l < iwyu_files.txt) source files."
110+
# Bundled boost-*.imp files declare overlapping symbols with
111+
# incompatible visibility, which trips an assert in IWYU's
112+
# include picker on every translation unit. Skip them; tdesktop
113+
# does not depend on boost in Telegram/SourceFiles.
114+
MAPPINGS=""
115+
for f in /usr/local/share/include-what-you-use/*.imp; do
116+
[ -e "$f" ] || continue
117+
case "$(basename "$f")" in boost*) continue;; esac
118+
MAPPINGS="$MAPPINGS -Xiwyu --mapping_file=$f"
119+
done
120+
# Project-specific mapping rules live next to this workflow's
121+
# helpers; load them in addition to the bundled IWYU mappings.
122+
for f in Telegram/build/iwyu/*.imp; do
123+
[ -e "$f" ] || continue
124+
MAPPINGS="$MAPPINGS -Xiwyu --mapping_file=$f"
125+
done
126+
echo "Mapping files: $MAPPINGS"
127+
xargs -a iwyu_files.txt iwyu_tool.py -p out -j $(nproc) -- $MAPPINGS > iwyu.out 2>&1 || true
128+
# iwyu_tool.py masks per-file failures, so the diff stage cannot
129+
# tell "nothing to clean up" from "every file crashed". Require
130+
# at least one file to have produced a real IWYU verdict.
131+
# `grep -c` already prints 0 when nothing matches and exits 1, so
132+
# `|| echo 0` would append a second "0" and yield a multi-line value
133+
# that breaks the integer comparison below. `|| true` swallows the
134+
# non-zero exit without adding output; the parameter expansion below
135+
# then covers the case where iwyu.out is missing entirely.
136+
analyzed=$(grep -cE "should (add|remove) these lines|has correct #includes" iwyu.out 2>/dev/null || true)
137+
analyzed=${analyzed:-0}
138+
echo "IWYU produced verdicts for $analyzed file blocks."
139+
if [ "$analyzed" -eq 0 ]; then
140+
echo "::error::IWYU produced no usable analysis for any file; see iwyu-diagnostics artifact."
141+
exit 1
142+
fi
143+
# Apply only the "should remove" half of IWYU's verdict via our
144+
# own helper, ignoring every "should add" suggestion. IWYU mostly
145+
# surfaces transitive includes as candidate additions, which would
146+
# make the cleanup PR noisy and stylistically off; we only want
147+
# deletions of headers IWYU found unused, and CI verifies they
148+
# really are.
149+
python3 Telegram/build/iwyu/apply_removals.py < iwyu.out
150+
SCRIPT
151+
docker run --rm \
152+
-u $(id -u) \
153+
-v $PWD:/usr/src/tdesktop \
154+
-w /usr/src/tdesktop \
155+
$IMAGE_TAG \
156+
bash -e iwyu.sh
157+
158+
- name: Upload diagnostics.
159+
if: always()
160+
uses: actions/upload-artifact@v7
161+
with:
162+
name: iwyu-diagnostics
163+
path: |
164+
iwyu.out
165+
iwyu_files.txt
166+
if-no-files-found: ignore
167+
168+
- name: Generate patch.
169+
id: diff
170+
run: |
171+
git diff -- Telegram/SourceFiles > iwyu.patch
172+
if [ -s iwyu.patch ]; then
173+
echo "has_changes=true" >> $GITHUB_OUTPUT
174+
echo "Patch size: $(wc -l < iwyu.patch) lines."
175+
git diff --stat -- Telegram/SourceFiles
176+
else
177+
echo "has_changes=false" >> $GITHUB_OUTPUT
178+
echo "No IWYU suggestions to apply, skipping."
179+
fi
180+
181+
- name: Upload patch artifact.
182+
if: steps.diff.outputs.has_changes == 'true'
183+
uses: actions/upload-artifact@v7
184+
with:
185+
name: iwyu-patch
186+
path: iwyu.patch
187+
188+
verify-linux:
189+
name: Verify Linux
190+
needs: generate
191+
if: needs.generate.outputs.has_changes == 'true'
192+
uses: ./.github/workflows/linux.yml
193+
with:
194+
ref: ${{ needs.generate.outputs.ref }}
195+
apply_patch: true
196+
patch_artifact: iwyu-patch
197+
force_depot: true
198+
upload_artifact: false
199+
200+
verify-mac:
201+
name: Verify macOS
202+
needs: generate
203+
if: needs.generate.outputs.has_changes == 'true'
204+
uses: ./.github/workflows/mac.yml
205+
with:
206+
ref: ${{ needs.generate.outputs.ref }}
207+
apply_patch: true
208+
patch_artifact: iwyu-patch
209+
force_depot: true
210+
upload_artifact: false
211+
212+
verify-win:
213+
name: Verify Windows
214+
needs: generate
215+
if: needs.generate.outputs.has_changes == 'true'
216+
uses: ./.github/workflows/win.yml
217+
with:
218+
ref: ${{ needs.generate.outputs.ref }}
219+
apply_patch: true
220+
patch_artifact: iwyu-patch
221+
force_depot: true
222+
upload_artifact: false
223+
224+
open-pr:
225+
name: Open pull request
226+
needs: [generate, verify-linux, verify-mac, verify-win]
227+
if: needs.generate.outputs.has_changes == 'true' && github.event_name != 'push'
228+
runs-on: ubuntu-latest
229+
230+
steps:
231+
- uses: actions/checkout@v6
232+
with:
233+
ref: ${{ needs.generate.outputs.ref }}
234+
fetch-depth: 0
235+
236+
- uses: actions/download-artifact@v5
237+
with:
238+
name: iwyu-patch
239+
path: ${{ runner.temp }}/iwyu
240+
241+
- name: Apply patch.
242+
run: git apply "${{ runner.temp }}/iwyu/iwyu.patch"
243+
244+
- name: Compute branch name.
245+
id: branch
246+
run: echo "name=iwyu/cleanup-$(date -u +%Y-%m)" >> $GITHUB_OUTPUT
247+
248+
- name: Create pull request.
249+
uses: peter-evans/create-pull-request@v7
250+
with:
251+
base: ${{ needs.generate.outputs.ref }}
252+
branch: ${{ steps.branch.outputs.name }}
253+
delete-branch: true
254+
commit-message: 'Drop unused includes detected by IWYU.'
255+
title: 'Drop unused includes (IWYU cleanup).'
256+
body: |
257+
Automated include-what-you-use cleanup of `Telegram/SourceFiles/`.
258+
259+
Verified to build on Linux, macOS and Windows by the upstream IWYU workflow run before this PR was opened:
260+
<https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}>
261+
262+
Note: GitHub does not auto-trigger PR checks for PRs created via `GITHUB_TOKEN`. Close and reopen this PR (or push an empty commit) to run the regular `linux.yml` / `mac.yml` / `win.yml` checks if you want to re-verify on the actual PR.

.github/workflows/win.yml

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,16 +66,14 @@ jobs:
6666

6767
windows:
6868
name: Windows
69-
runs-on: ${{ matrix.arch == 'arm64' && 'windows-11-arm' || ((inputs.force_depot || github.event_name == 'pull_request' || startsWith(github.ref, 'refs/tags/')) && 'depot-windows-latest-16' || 'windows-latest') }}
69+
runs-on: ${{ (inputs.force_depot || github.event_name == 'pull_request' || startsWith(github.ref, 'refs/tags/')) && 'depot-windows-latest-16' || 'windows-latest' }}
7070

7171
strategy:
7272
matrix:
73-
arch: [x64_x86, x64, arm64]
73+
arch: [x64_x86, x64]
7474
qt: ["", qt6]
7575
generator: ["", "Ninja Multi-Config"]
7676
exclude:
77-
- arch: arm64
78-
qt: ""
7977
- arch: x64_x86
8078
qt: qt6
8179

@@ -195,7 +193,6 @@ jobs:
195193
if [ -n "${{ matrix.arch }}" ]; then
196194
case "${{ matrix.arch }}" in
197195
x64_x86) ARCH="x86";;
198-
arm64) ARCH="arm";;
199196
*) ARCH="${{ matrix.arch }}";;
200197
esac
201198
echo "Architecture from matrix: $ARCH"
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
#!/usr/bin/env python3
2+
"""Apply only "should remove" suggestions from an IWYU report.
3+
4+
Reads an IWYU run output on stdin, deletes the indicated #include lines
5+
from the referenced files in place. Every "should add these lines"
6+
suggestion is ignored.
7+
8+
Rationale: IWYU's "include what you use" mantra produces patches that
9+
are mostly *additions* (transitive includes made explicit). For an
10+
automated cleanup PR we only want safe deletions of includes the
11+
analyser found unused; CI verifies the result still builds.
12+
"""
13+
14+
import re
15+
import sys
16+
from pathlib import Path
17+
18+
HEADER_RE = re.compile(r'^(\S.*?) should remove these lines:$')
19+
LINE_RE = re.compile(
20+
r'^- #include\s+\S+.*//\s*lines\s+(\d+)-(\d+)\s*$'
21+
)
22+
23+
24+
def collect_removals(stream):
25+
removals = {}
26+
current_path = None
27+
in_block = False
28+
for raw in stream:
29+
line = raw.rstrip('\n')
30+
match = HEADER_RE.match(line)
31+
if match:
32+
current_path = match.group(1)
33+
removals.setdefault(current_path, set())
34+
in_block = True
35+
continue
36+
if not in_block:
37+
continue
38+
if not line.strip():
39+
in_block = False
40+
current_path = None
41+
continue
42+
match = LINE_RE.match(line.strip())
43+
if match:
44+
start, end = int(match.group(1)), int(match.group(2))
45+
removals[current_path].update(range(start, end + 1))
46+
return removals
47+
48+
49+
def apply_removals(removals):
50+
files_touched = 0
51+
for path_str, line_numbers in removals.items():
52+
if not line_numbers:
53+
continue
54+
path = Path(path_str)
55+
if not path.is_file():
56+
print(
57+
f'apply_removals: skip {path_str} (not found)',
58+
file=sys.stderr,
59+
)
60+
continue
61+
original = path.read_text().splitlines(keepends=True)
62+
kept = [
63+
line for index, line in enumerate(original, start=1)
64+
if index not in line_numbers
65+
]
66+
if len(kept) != len(original):
67+
path.write_text(''.join(kept))
68+
files_touched += 1
69+
return files_touched
70+
71+
72+
def main():
73+
removals = collect_removals(sys.stdin)
74+
touched = apply_removals(removals)
75+
total_lines = sum(len(s) for s in removals.values())
76+
print(
77+
f'apply_removals: stripped {total_lines} include line(s) '
78+
f'from {touched} file(s).',
79+
file=sys.stderr,
80+
)
81+
82+
83+
if __name__ == '__main__':
84+
main()

Telegram/build/iwyu/tdesktop.imp

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# IWYU mapping rules for Telegram Desktop.
2+
#
3+
# Loaded automatically by .github/workflows/iwyu.yml. Add new rules here
4+
# whenever the IWYU cleanup workflow keeps suggesting includes that are
5+
# stylistically wrong or that bypass the project's own wrapper headers.
6+
#
7+
# Format reference:
8+
# https://github.com/include-what-you-use/include-what-you-use/blob/master/docs/IWYUMappings.md
9+
#
10+
# Each include rule is a four-element list:
11+
# [from-header, from-visibility, to-header, to-visibility]
12+
# where visibility is "public" or "private". A "private" header is one
13+
# IWYU should never suggest directly; the matching "public" header is
14+
# what it should suggest in its place.
15+
16+
[
17+
# `lang_auto.h` is generated from `Resources/langs/*.strings`; the
18+
# public entry point is `lang/lang_keys.h`, which also pulls in
19+
# helpers like `lang_text_entity.h`. Both bracket forms are listed
20+
# because IWYU emits whichever matches how the include path was
21+
# advertised to the compiler.
22+
{ "include": ["<lang_auto.h>", "private", "\"lang/lang_keys.h\"", "public"] },
23+
{ "include": ["\"lang_auto.h\"", "private", "\"lang/lang_keys.h\"", "public"] },
24+
]

0 commit comments

Comments
 (0)