Repository navigation
393 lines (391 loc) · 17.9 KB
/
Copy pathci.yml
File metadata and controls
393 lines (391 loc) · 17.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
name: CI
on:
push:
branches:
- master
pull_request:
branches:
- master
# One run per branch at a time: pushing again while a run is going makes that run pointless, and
# the queued macOS jobs it holds are what everything else is waiting behind.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
# What a pull request touches, so the jobs that cost the most can sit out the ones that cannot
# break them. A translation or documentation pull request still gets the Windows tests and the
# Android checks, which compile every catalogue; it does not need an iPhone simulator.
# A push to master always runs everything.
changes:
name: What changed
runs-on: ubuntu-latest
outputs:
code: ${{ steps.check.outputs.code }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- id: check
run: |
if [ "${{ github.event_name }}" != "pull_request" ]; then
echo "code=true" >> "$GITHUB_OUTPUT"
exit 0
fi
files=$(git diff --name-only "origin/${{ github.base_ref }}...HEAD")
if echo "$files" | grep -qvE '^(po/|doc/|web/|README\.md$)'; then
echo "code=true" >> "$GITHUB_OUTPUT"
else
echo "code=false" >> "$GITHUB_OUTPUT"
fi
fmt:
name: Format check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
submodules: recursive
# Pinned: rustfmt's own output changes between nightlies, and a floating one turns that
# into master going red on a day nobody touched the formatting. Bumping this is then a
# deliberate change with the reformat in the same commit.
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: nightly-2026-10-05
components: rustfmt
- name: Check formatting
run: cargo fmt --check
android:
name: Android checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Set up JDK 17
uses: actions/setup-java@v6
with:
java-version: '25'
distribution: 'temurin'
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
# Writing the cache takes about five minutes. Only master writes one; a pull request
# restores it and skips the write, which is the slowest step in several of these jobs.
save-if: ${{ github.ref == 'refs/heads/master' }}
# The unit tests run on the host JVM and never call into the native library, but the code
# they cover is compiled against the uniffi types, so the bindings have to exist for the
# app module to compile at all. Generating them is a host build of paperback-core alone:
# no NDK or cargo-ndk, and none of the pdfium, translation, or Help assets that the APK
# build in build.yml needs on top. ktlint skips the generated file either way.
# The interface lives in the Rust now rather than in a .udl, so uniffi reads it from the
# compiled library's own metadata. That means building the library first, and building it
# unstripped: the release profile sets strip = true, which removes the very symbols the
# interface is read from.
- name: Generate Kotlin bindings
run: |
cargo build -p paperback-core --features uniffi --lib
LIB=$(ls target/debug/libpaperback_core.so target/debug/libpaperback_core.dylib target/debug/paperback_core.dll 2>/dev/null | head -1)
cargo run -p paperback-core --features uniffi --bin uniffi-bindgen -- generate --library "$LIB" --language kotlin --out-dir android/app/src/main/kotlin --no-format
- name: Cache Gradle packages
uses: actions/cache@v6
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ hashFiles('android/**/*.gradle.kts', 'android/gradle/wrapper/gradle-wrapper.properties') }}
restore-keys: gradle-
- name: Check formatting and run unit tests
working-directory: android
run: |
chmod +x gradlew
./gradlew ktlintCheck testDebugUnitTest
- name: Upload test report
if: failure()
uses: actions/upload-artifact@v7
with:
name: android-test-report
path: android/app/build/reports/tests/
retention-days: 7
android-instrumented:
name: Android instrumented tests
needs: changes
if: needs.changes.outputs.code == 'true'
# Split from the unit test job because booting an emulator costs minutes that the rest of
# the Android checks should not wait on.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Set up JDK
uses: actions/setup-java@v6
with:
java-version: '25'
distribution: 'temurin'
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
# Writing the cache takes about five minutes. Only master writes one; a pull request
# restores it and skips the write, which is the slowest step in several of these jobs.
save-if: ${{ github.ref == 'refs/heads/master' }}
# Same reasoning as the unit test job: the app module only compiles once the uniffi types
# exist, and generating them is a host build of paperback-core with no NDK involved.
# The interface lives in the Rust now rather than in a .udl, so uniffi reads it from the
# compiled library's own metadata. That means building the library first, and building it
# unstripped: the release profile sets strip = true, which removes the very symbols the
# interface is read from.
- name: Generate Kotlin bindings
run: |
cargo build -p paperback-core --features uniffi --lib
LIB=$(ls target/debug/libpaperback_core.so target/debug/libpaperback_core.dylib target/debug/paperback_core.dll 2>/dev/null | head -1)
cargo run -p paperback-core --features uniffi --bin uniffi-bindgen -- generate --library "$LIB" --language kotlin --out-dir android/app/src/main/kotlin --no-format
- name: Cache Gradle packages
uses: actions/cache@v6
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ hashFiles('android/**/*.gradle.kts', 'android/gradle/wrapper/gradle-wrapper.properties') }}
restore-keys: gradle-
- name: Enable KVM for the emulator
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
# The reading bar's semantics are what TalkBack reads out, and they only exist once the
# bar is composed, so this is the only place that behaviour can be checked automatically.
- name: Run instrumented tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 34
target: google_apis
arch: x86_64
disable-animations: true
script: cd android && chmod +x gradlew && ./gradlew connectedDebugAndroidTest
- name: Upload instrumented test report
if: failure()
uses: actions/upload-artifact@v7
with:
name: android-instrumented-report
path: android/app/build/reports/androidTests/
retention-days: 7
ios:
name: iOS checks
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: macos-latest
steps:
- uses: actions/checkout@v7
with:
submodules: recursive
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
targets: aarch64-apple-ios,aarch64-apple-ios-sim
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
# Writing the cache takes about five minutes. Only master writes one; a pull request
# restores it and skips the write, which is the slowest step in several of these jobs.
save-if: ${{ github.ref == 'refs/heads/master' }}
# The XCFramework and the Swift bindings both have to exist before xcodebuild runs: the
# app imports the generated paperback.swift, and the project links the framework. This is
# the same task the release build uses, so a break in either shows up here.
# The framework is the Rust side of the app, so it only changes when that does. Keyed on
# everything `cargo xtask ios` reads: the Rust sources and lockfile, the task itself, and
# the catalogues and readmes it turns into the app's strings and help.
- name: Cache the built XCFramework
id: xcframework
uses: actions/cache@v6
with:
path: |
ios/paperbackFFI.xcframework
ios/libpdfium.framework
ios/libpdfium.dylib
ios/Paperback/Generated
ios/Paperback/Readmes
ios/Paperback/*.lproj
key: xcframework-${{ hashFiles('Cargo.lock', 'crates/**/*.rs', 'crates/xtask/src/ios.rs', 'po/*.po', 'doc/readme*.md') }}
- name: Build the XCFramework and Swift bindings
if: steps.xcframework.outputs.cache-hit != 'true'
run: cargo xtask ios --release
# No signing: CI has no certificate, and an unsigned build still compiles and links every
# Swift file, which is what this job is for. Producing an .ipa needs the release flow.
- name: Build the app
run: >
xcodebuild -project ios/Paperback.xcodeproj -scheme Paperback
-configuration Release -destination 'generic/platform=iOS'
CODE_SIGNING_ALLOWED=NO build
# The unit tests are hosted by the app, so they need a simulator to run in. Picking the
# first available iPhone by udid rather than naming a model keeps this working when the
# runner image changes which devices it ships.
- name: Run the unit tests
run: |
UDID=$(xcrun simctl list devices available -j | python3 -c "import json,sys; d=json.load(sys.stdin)['devices']; print(next(x['udid'] for rt in d for x in d[rt] if 'iPhone' in x['name']))")
echo "Testing on simulator $UDID"
xcodebuild -project ios/Paperback.xcodeproj -scheme Paperback -destination "id=$UDID" CODE_SIGNING_ALLOWED=NO test
test:
name: Tests
runs-on: windows-latest
steps:
- uses: actions/checkout@v7
with:
submodules: recursive
fetch-depth: 0
- name: Setup MSVC
uses: ilammy/msvc-dev-cmd@v1
with:
arch: x64
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
- name: Install CMake and Ninja
run: choco install cmake ninja -y
- name: Install WebView2 SDK
run: |
nuget install Microsoft.Web.WebView2 -OutputDirectory packages
$pkg = Get-ChildItem "packages" -Filter "Microsoft.Web.WebView2.*" -Directory | Select-Object -First 1
$libPath = Join-Path $pkg.FullName "build\native\x64"
echo "LIB=$env:LIB;$libPath" >> $env:GITHUB_ENV
- name: Install Gettext via MSYS2
id: msys2
uses: msys2/setup-msys2@v2
with:
msystem: UCRT64
update: true
install: >-
gettext
mingw-w64-ucrt-x86_64-gettext-tools
- name: Cache Rust dependencies
id: rust-cache
uses: Swatinem/rust-cache@v2
with:
# Writing the cache takes about five minutes. Only master writes one; a pull request
# restores it and skips the write, which is the slowest step in several of these jobs.
save-if: ${{ github.ref == 'refs/heads/master' }}
# A pull request that changes Cargo.lock restores a partial cache, and wxdragon-sys then rebuilds wxWidgets on top of the old CMake tree. The debug .pdb files that leaves behind are corrupt, and the UI tests' link of paperback-core fails on them (LNK1103). Building wxWidgets afresh on a miss avoids that.
- name: Rebuild wxWidgets on a partial cache
if: steps.rust-cache.outputs.cache-hit != 'true'
shell: bash
run: |
rm -rf target/debug/wxwidgets_cmake_build target/debug/wxdragon_sys_cmake_build
cargo clean -p wxdragon-sys
# msgfmt comes from MSYS2, which setup-msys2 leaves off this step's PATH, so the build
# scripts would silently skip every .po file. Appended rather than prepended: MSYS2's
# usr/bin has a coreutils link.exe that would otherwise shadow the MSVC linker.
# The wxWidgets commit .cargo/config.toml pins in WXWIDGETS_DIR, cached by that pin.
- name: Cache the pinned wxWidgets source
uses: actions/cache@v6
with:
path: target/wxWidgets-*
key: wxwidgets-src-${{ hashFiles('.cargo/config.toml') }}
- name: Fetch the pinned wxWidgets source
run: cargo xtask wxwidgets
- name: Run tests
run: |
$env:PATH = "$env:PATH;${{ steps.msys2.outputs.msys2-location }}\ucrt64\bin;${{ steps.msys2.outputs.msys2-location }}\usr\bin"
cargo test --workspace
# `--workspace` rather than `-p paperback`: selecting one package resolves features
# differently, so the run above is rebuilt from scratch, which cost more than eight minutes
# to reach four tests that take two seconds each. Only the UI tests are ignored, so this
# named by target rather than with a bare --ignored, which would also run the deferred
# parser tests that are ignored because they do not pass yet.
- name: Run UI tests
run: |
$env:PATH = "$env:PATH;${{ steps.msys2.outputs.msys2-location }}\ucrt64\bin;${{ steps.msys2.outputs.msys2-location }}\usr\bin"
cargo test --workspace --test opens_document --test status_bar --test restores_last_document --test heading_announcements -- --ignored
test-macos:
name: Tests (macOS)
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: macos-latest
steps:
- uses: actions/checkout@v7
with:
submodules: recursive
fetch-depth: 0
- name: Install CMake, Ninja, and Gettext
run: |
brew install cmake ninja gettext
echo "/opt/homebrew/opt/gettext/bin" >> "$GITHUB_PATH"
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
components: clippy
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
# Writing the cache takes about five minutes. Only master writes one; a pull request
# restores it and skips the write, which is the slowest step in several of these jobs.
save-if: ${{ github.ref == 'refs/heads/master' }}
# The wxWidgets commit .cargo/config.toml pins in WXWIDGETS_DIR, cached by that pin.
- name: Cache the pinned wxWidgets source
uses: actions/cache@v6
with:
path: target/wxWidgets-*
key: wxwidgets-src-${{ hashFiles('.cargo/config.toml') }}
- name: Fetch the pinned wxWidgets source
run: cargo xtask wxwidgets
- name: Run tests
run: cargo test --workspace
# The default profile shares the wxWidgets build with the workspace tests.
- name: Run clippy
run: cargo clippy --workspace --all-targets
clippy:
name: Clippy
runs-on: windows-latest
steps:
- uses: actions/checkout@v7
with:
submodules: recursive
fetch-depth: 0
- name: Setup MSVC
uses: ilammy/msvc-dev-cmd@v1
with:
arch: x64
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
components: clippy
- name: Install CMake and Ninja
run: choco install cmake ninja -y
- name: Install WebView2 SDK
run: |
nuget install Microsoft.Web.WebView2 -OutputDirectory packages
$pkg = Get-ChildItem "packages" -Filter "Microsoft.Web.WebView2.*" -Directory | Select-Object -First 1
$libPath = Join-Path $pkg.FullName "build\native\x64"
echo "LIB=$env:LIB;$libPath" >> $env:GITHUB_ENV
- name: Install Gettext via MSYS2
id: msys2
uses: msys2/setup-msys2@v2
with:
msystem: UCRT64
update: true
install: >-
gettext
mingw-w64-ucrt-x86_64-gettext-tools
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
# Writing the cache takes about five minutes. Only master writes one; a pull request
# restores it and skips the write, which is the slowest step in several of these jobs.
save-if: ${{ github.ref == 'refs/heads/master' }}
# Needed whenever wxdragon-sys has to build wxWidgets again, such as after its features change, which a restored cache otherwise hides.
- name: Cache the pinned wxWidgets source
uses: actions/cache@v6
with:
path: target/wxWidgets-*
key: wxwidgets-src-${{ hashFiles('.cargo/config.toml') }}
- name: Fetch the pinned wxWidgets source
run: cargo xtask wxwidgets
# `clippy::all` is denied through [workspace.lints] in Cargo.toml, so this fails the job on
# any of those lints. The pedantic and nursery groups stay warnings and are printed, not
# enforced -- see the comment on the lint table for why.
# msgfmt comes from MSYS2, which setup-msys2 leaves off this step's PATH, so the build
# scripts would silently skip every .po file. Appended rather than prepended: MSYS2's
# usr/bin has a coreutils link.exe that would otherwise shadow the MSVC linker.
- name: Run clippy
run: |
$env:PATH = "$env:PATH;${{ steps.msys2.outputs.msys2-location }}\ucrt64\bin;${{ steps.msys2.outputs.msys2-location }}\usr\bin"
cargo clippy --workspace --all-targets --release