Skip to content

Commit ec8c98e

Browse files
authored
Merge pull request #315 from winnerspiros/copilot/check-build-errors-and-warnings
fix(android): silence ILLink IL1012 crash and AVIF warning spam in Android release build
2 parents f34ef00 + f5cbaa9 commit ec8c98e

3 files changed

Lines changed: 46 additions & 4 deletions

File tree

.github/resource-optimizer/config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"png_webp_lossy_method": 6,
2020
"jpeg_webp_quality": 88,
2121
"jpeg_webp_method": 6,
22-
"jpeg_avif_enabled": true,
22+
"jpeg_avif_enabled": false,
2323
"jpeg_avif_crf": 30,
2424
"jpeg_avif_preset": 4,
2525
"audio_codec": "libopus",

osu.Android.props

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,13 @@
9393
assembly that uses runtime reflection, making this "manual trim" rather than
9494
automatic: the linker strips only what we knowingly leave out of Linker.xml. -->
9595
<AndroidLinkMode>Full</AndroidLinkMode>
96+
<!-- Suppress ILLink trim-analysis warnings (IL2xxx series).
97+
ILLink 10.0.7 has a bug in MessageOrigin.ToString() that calls .Last() on an
98+
empty source-location sequence, throwing InvalidOperationException and crashing
99+
the trimmer with IL1012 "Fatal error in IL Linker" when any of these warnings is
100+
flushed. We use a curated Linker.xml for manual preservation, so the IL2xxx
101+
hints serve no guidance purpose — suppressing them avoids the crash entirely. -->
102+
<SuppressTrimAnalysisWarnings>true</SuppressTrimAnalysisWarnings>
96103
<!-- Keep IL alongside profiled-AOT native code so any method outside the bundled
97104
AOT profile has a JIT fallback (instead of MissingMethodException at first
98105
call). The .NET Android SDK default is 'false' for non-trimmed builds;

scripts/optimize_resource_overrides.py

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,36 @@ def run_ffmpeg(args: list[str], output_log_level: str = "error") -> None:
2828
subprocess.run(["ffmpeg", "-y", "-loglevel", output_log_level, *args], check=True, capture_output=True, text=True)
2929

3030

31+
_svtav1_available_cache: Optional[bool] = None
32+
33+
34+
def is_svtav1_available() -> bool:
35+
"""Return True iff the running ffmpeg binary has a working libsvtav1 encoder.
36+
37+
Probes by attempting to encode a single 16×16 black frame to a null sink.
38+
Result is cached in a module-level variable so the probe runs at most once.
39+
"""
40+
global _svtav1_available_cache
41+
if _svtav1_available_cache is None:
42+
try:
43+
subprocess.run(
44+
[
45+
"ffmpeg", "-y", "-loglevel", "error",
46+
"-f", "lavfi", "-i", "color=black:size=16x16:rate=1:duration=0.1",
47+
"-c:v", "libsvtav1",
48+
"-pix_fmt", "yuv420p",
49+
"-f", "null", "-",
50+
],
51+
check=True,
52+
capture_output=True,
53+
text=True,
54+
)
55+
_svtav1_available_cache = True
56+
except (subprocess.CalledProcessError, FileNotFoundError):
57+
_svtav1_available_cache = False
58+
return _svtav1_available_cache
59+
60+
3161
def should_include(path: Path, include_globs: list[str], exclude_globs: list[str], root: Path) -> bool:
3262
relative = path.relative_to(root).as_posix()
3363
included = any(fnmatch.fnmatch(relative, pattern) for pattern in include_globs)
@@ -147,8 +177,10 @@ def convert_image(source: Path, config: dict, relative_path: str) -> tuple[Path,
147177
# silently strips alpha channels, producing a tiny but completely transparent
148178
# output. PNG files without alpha and all JPEG sources are safe to encode as
149179
# AVIF because yuv420p has no alpha plane.
180+
# is_svtav1_available() probes once and caches the result, so no per-file
181+
# overhead and no per-file warnings when the encoder is absent.
150182
can_use_avif = ext in {".jpg", ".jpeg"} or (ext == ".png" and not has_alpha)
151-
if can_use_avif and bool(config.get("jpeg_avif_enabled", False)):
183+
if can_use_avif and bool(config.get("jpeg_avif_enabled", False)) and is_svtav1_available():
152184
avif_crf = int(config.get("jpeg_avif_crf", 30))
153185
avif_preset = int(config.get("jpeg_avif_preset", 4))
154186
p_avif = source.parent / f".{source.stem}.lossy.avif.tmp"
@@ -167,8 +199,6 @@ def convert_image(source: Path, config: dict, relative_path: str) -> tuple[Path,
167199
)
168200
temp_candidates.append((p_avif, f"{src_label}-avif-svtav1-crf{avif_crf}-p{avif_preset}", False, ".avif"))
169201
except subprocess.CalledProcessError:
170-
# Log a warning so the operator knows AVIF was requested but unavailable.
171-
print(f"::warning::AVIF requested for {relative_path} but libsvtav1 encode failed; falling back to WebP.")
172202
p_avif.unlink(missing_ok=True)
173203

174204
# ── Pick the smallest candidate that meets the SSIM threshold ────────────
@@ -298,6 +328,11 @@ def optimize(root: Path, config: dict, dry_run: bool) -> dict:
298328
allow_video = bool(config.get("enable_video_conversion", True))
299329
keep_originals = bool(config.get("keep_original_files", True))
300330

331+
# Probe libsvtav1 once upfront so we emit at most one notice rather than
332+
# one warning per AVIF-eligible file when the encoder is absent.
333+
if bool(config.get("jpeg_avif_enabled", False)) and not is_svtav1_available():
334+
print("::notice::jpeg_avif_enabled is true but libsvtav1 is unavailable; AVIF skipped, falling back to WebP.")
335+
301336
results: list[ConversionResult] = []
302337
skipped: list[str] = []
303338

0 commit comments

Comments
 (0)