forked from ppy/osu
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathLinker.xml
More file actions
275 lines (250 loc) · 21 KB
/
Copy pathLinker.xml
File metadata and controls
275 lines (250 loc) · 21 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
<?xml version="1.0" encoding="utf-8"?>
<!--
Manual ILLink / .NET Android linker preservation descriptor for osu! Android.
Context
───────
AndroidLinkMode=Full (set in osu.Android.props) enables .NET's ILLink trimmer
across all assemblies, not just the Android SDK wrapper (Mono.Android.dll).
Without this file, ILLink would eagerly strip types it cannot prove are
reachable through normal call graphs — which breaks any assembly that discovers
types or members at runtime via reflection (Realm's ORM schema, Newtonsoft.Json
contract builders, AutoMapper profile scanners, SignalR hub proxies, etc.).
This file replaces the risk of "automatic trim" with a curated per-assembly
declaration:
• preserve="all" — keep every type, field and method in this assembly.
Used for assemblies that use runtime reflection to discover their own or
other types, where a statically-invisible call path can cause a
MissingMethodException or TypeLoadException at runtime.
• (absent) — the linker will tree-shake the assembly, keeping only
types actually reachable from the preserved set. Safe for pure-algorithmic
libraries that never look up types by name.
Savings over SdkOnly (the previous setting)
────────────────────────────────────────────
SdkOnly only strips unused Java-wrapper types in Mono.Android.dll.
Full + this file additionally strips:
• Unused types in all .NET BCL assemblies (System.*, Microsoft.*).
• ~80 % of NUnit — osu.Game carries test-scene base classes, but the linker
keeps only the NUnit.Framework types actually referenced in osu.Game.dll's
IL, discarding most of the assertion, runner, and constraint sub-systems.
• Unused types in DiffPlex, SharpCompress, and Microsoft.Toolkit.HighPerformance
(all zero-reflection, pure-generic/algorithmic libraries).
• Any additional Mono.Android.dll types that SdkOnly left behind.
How to maintain
───────────────
If a runtime TypeLoadException or MissingMethodException appears in a new
package after this file was last updated, add the offending assembly here with
preserve="all". Always add a comment explaining why reflection makes the full
preservation necessary.
-->
<linker>
<!-- ═══════════════════════════════════════════════════════════════════════
osu! first-party assemblies
═══════════════════════════════════════════════════════════════════════
All use reflection heavily:
• osu.Game / rulesets: ruleset discovery via RulesetStore.GetRuleset(),
DI container (DependencyContainer uses ConstructorInfo), Bindable<T>
value converters, settings / mod attributes, beatmap decoder registry,
score serialiser, skin component lookup.
• osu.Android: Android Activity / Fragment reflection, P/Invoke
attribute scanning in native bridge, ADPF session manager. -->
<assembly fullname="osu.Android" preserve="all" />
<assembly fullname="osu.Game" preserve="all" />
<assembly fullname="osu.Game.Rulesets.Osu" preserve="all" />
<assembly fullname="osu.Game.Rulesets.Taiko" preserve="all" />
<assembly fullname="osu.Game.Rulesets.Catch" preserve="all" />
<assembly fullname="osu.Game.Rulesets.Mania" preserve="all" />
<!-- ═══════════════════════════════════════════════════════════════════════
ppy framework
═══════════════════════════════════════════════════════════════════════
osu.Framework: DrawableComponentLookup and SourceGeneratedImplementationLookup
use Type.GetType()/Activator.CreateInstance() for draw hierarchy resolution.
Shader attribute scanning, TextureLoaderStore fallback chain, bindable system,
resource store extension routing — all reflection-dependent.
osu.Framework.Android: Android-specific host, input handling, surface wrappers.
AndroidGameActivity is the abstract base of OsuGameActivity but does NOT declare
its own (IntPtr, JniHandleOwnership) JNI-activation constructor — that constructor
lives on its SDLActivity grandparent (in the SDL3-CS assembly, listed below).
Without preserve="all", ILLink trims reflection-only members such as
RegisterAttribute-driven type-binding and generated JNI glue — none of which have
a managed call-graph path.
SDL3-CS (ppy.SDL3-CS): Contains Org.Libsdl.App.SDLActivity, the Java-binding class
from which AndroidGameActivity (and transitively OsuGameActivity) inherits.
SDLActivity's JNI-activation constructors — in particular the
(IntPtr, JniHandleOwnership) overload used by TypeManager.Activate to wrap the
freshly-created Android Activity Java object in a managed peer — are never called
from managed code, so ILLink has no static call-graph path to them and strips them
under AndroidLinkMode=Full. The resulting crash is exactly the
"Could not activate JNI Handle ... as managed type 'osu.Android.OsuGameActivity'"
NotSupportedException seen in the v2026.508.226 and v2026.508.228 field logs.
(The v2026.508.228 fix added osu.Framework.Android to Linker.xml and a
[DynamicDependency] on OsuGameActivity() but missed SDL3-CS entirely, because the
assumption was that AndroidGameActivity carried the JNI ctor — it does not.)
ppy.Veldrid.SPIRV: SPIR-V cross-compiler; dynamically loads shader reflector types.
(NuGet package ID and assembly name are both ppy.Veldrid.SPIRV — no mismatch.)
IMPORTANT — assembly name vs. NuGet package ID:
ILLink's <assembly fullname="..."> descriptor element matches against the CLR
assembly name (i.e. the value of <AssemblyName> in the project file), NOT the
NuGet package ID. These two values are NOT always the same. In particular:
NuGet package ID → CLR assembly name (AssemblyName in csproj)
ppy.osu.Framework → osu.Framework
ppy.osu.Framework.Android → osu.Framework.Android
ppy.SDL3-CS → SDL3-CS
ppy.Veldrid.SPIRV → ppy.Veldrid.SPIRV (same — no ppy. mismatch here)
Using the NuGet package ID instead of the CLR assembly name causes the preserve rule
to be stored but never matched.
NOTE: osu.Framework, osu.Framework.Android, and SDL3-CS originate from NuGet
packages and are not visible in ILLink's assembly search path at descriptor-
processing time, causing IL2007 ("Could not resolve assembly") for each entry.
In theory ILLink stores the rule for later application; in practice the deferred
mechanism has proven unreliable with ILLink 10.0.x + NuGet-resolved assemblies
(evidenced by the v2026.508.228 field crash being identical to v2026.508.226 despite
these entries being present). The primary protection is therefore AndroidLinkSkip
in osu.Android.props, which bypasses ILLink entirely for these assemblies; these
Linker.xml entries and the [DynamicDependency] attributes on OsuGameActivity serve
as secondary and tertiary backstops respectively. -->
<assembly fullname="osu.Framework" preserve="all" />
<assembly fullname="osu.Framework.Android" preserve="all" />
<assembly fullname="SDL3-CS" preserve="all" />
<assembly fullname="ppy.Veldrid.SPIRV" preserve="all" />
<!-- ═══════════════════════════════════════════════════════════════════════
Audio (ManagedBass)
═══════════════════════════════════════════════════════════════════════
ManagedBass uses [DllImport] / P/Invoke attribute discovery. The
winnerspiros fork adds UnmanagedCallersOnly / NativeCallable methods that
must survive trimming so the native bridge can resolve them. -->
<assembly fullname="ManagedBass" preserve="all" />
<assembly fullname="ManagedBass.Fx" preserve="all" />
<assembly fullname="ManagedBass.Mix" preserve="all" />
<!-- ═══════════════════════════════════════════════════════════════════════
Database — Realm
═══════════════════════════════════════════════════════════════════════
Realm's source generator emits IRealmObjectHelper implementations at
compile time, but at runtime it discovers RealmObject schemas by
reflecting over IRealmObject-derived types and their [MapTo]/[Indexed]
attributes. Stripping any Realm type (or any osu RealmObject property)
causes a RealmException at database open time.
NOTE: Realms.PlatformHelpers was a separate assembly in older Realm SDK
versions (≤ 10.x) and was merged into Realm in v11. Realm 20.x does not
ship it as a standalone assembly, so the entry is omitted here. -->
<assembly fullname="Realm" preserve="all" />
<!-- ═══════════════════════════════════════════════════════════════════════
JSON / binary serialisation
═══════════════════════════════════════════════════════════════════════
Newtonsoft.Json: JsonSerializer builds DefaultContractResolver at runtime
by reflecting over target types (properties, [JsonProperty] attrs, etc.).
Every BeatmapOnlineMetadata, APIUser, Mod subclass etc. must be present.
MessagePack: resolver chains are built at startup by scanning assemblies
for [MessagePackObject]-annotated types. MessagePackSerializer.Typeless
uses Type.GetType() for polymorphic deserialisation.
MessagePack.Annotations: attribute types ([Key], [MessagePackObject], …)
must survive so the reflection scanner can read them. -->
<assembly fullname="Newtonsoft.Json" preserve="all" />
<assembly fullname="MessagePack" preserve="all" />
<assembly fullname="MessagePack.Annotations" preserve="all" />
<!-- ═══════════════════════════════════════════════════════════════════════
Object mapping — AutoMapper
═══════════════════════════════════════════════════════════════════════
AutoMapper's MappingExpression<TSource, TDest> scans source/dest type
hierarchies for matching property names at IMapper.Map<T>() call sites.
Profile types are discovered via Assembly.GetTypes() at startup. -->
<assembly fullname="AutoMapper" preserve="all" />
<!-- ═══════════════════════════════════════════════════════════════════════
Error reporting — Sentry
═══════════════════════════════════════════════════════════════════════
Sentry enriches events with stack frames using MethodBase.GetCurrentMethod()
and Type.FullName. SentryOptions.AddIntegration() discovers integrations by
type name. SentryEvent breadcrumbs carry dynamic type information. -->
<assembly fullname="Sentry" preserve="all" />
<!-- ═══════════════════════════════════════════════════════════════════════
Real-time networking — SignalR
═══════════════════════════════════════════════════════════════════════
The SignalR client generates hub proxies at runtime using
DispatchProxy.Create<T>() and discovers hub method overloads by
MethodInfo.GetParameters() reflection. Protocol negotiation selects
serializers by type name. All layers of the SignalR stack must be
fully preserved to avoid MissingMethodException on first multiplayer
connection. -->
<assembly fullname="Microsoft.AspNetCore.SignalR.Client" preserve="all" />
<assembly fullname="Microsoft.AspNetCore.SignalR.Client.Core" preserve="all" />
<assembly fullname="Microsoft.AspNetCore.SignalR.Common" preserve="all" />
<assembly fullname="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" preserve="all" />
<assembly fullname="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" preserve="all" />
<assembly fullname="Microsoft.AspNetCore.Http.Connections.Client" preserve="all" />
<assembly fullname="Microsoft.AspNetCore.Http.Connections.Common" preserve="all" />
<!-- ═══════════════════════════════════════════════════════════════════════
Dependency injection and configuration — Microsoft.Extensions.*
═══════════════════════════════════════════════════════════════════════
The DI container (IServiceProvider / ServiceCollection) scans constructor
parameters via ConstructorInfo.GetParameters() to wire services at runtime.
IOptions<T> binders use PropertyInfo reflection to populate typed config
objects from key-value pairs. ILoggerFactory finds providers by type.
NOTE: Microsoft.Extensions.Configuration (non-Abstractions) and
Microsoft.Extensions.Http are NOT directly referenced by this project and
are not shipped as standalone assemblies in .NET 10 (their APIs are in-box
or merged into the Abstractions layer). Adding them here would generate
"Could not resolve assembly" linker warnings with no preservation benefit. -->
<assembly fullname="Microsoft.Extensions.DependencyInjection" preserve="all" />
<assembly fullname="Microsoft.Extensions.DependencyInjection.Abstractions" preserve="all" />
<assembly fullname="Microsoft.Extensions.Logging" preserve="all" />
<assembly fullname="Microsoft.Extensions.Logging.Abstractions" preserve="all" />
<assembly fullname="Microsoft.Extensions.Options" preserve="all" />
<assembly fullname="Microsoft.Extensions.Configuration.Abstractions" preserve="all" />
<assembly fullname="Microsoft.Extensions.Primitives" preserve="all" />
<!-- ═══════════════════════════════════════════════════════════════════════
SQLite stack
═══════════════════════════════════════════════════════════════════════
SQLitePCLRaw uses a runtime provider discovery pattern: a static factory
field is populated via reflection from the bundle assembly at startup.
Microsoft.Data.Sqlite uses SqliteConnection which resolves the native
sqlite3 library by scanning the loaded SQLitePCLRaw provider. -->
<assembly fullname="SQLitePCLRaw.core" preserve="all" />
<assembly fullname="SQLitePCLRaw.provider.e_sqlite3" preserve="all" />
<assembly fullname="SQLitePCLRaw.batteries_v2" preserve="all" />
<assembly fullname="Microsoft.Data.Sqlite" preserve="all" />
<!-- ═══════════════════════════════════════════════════════════════════════
Platform integration — MAUI Essentials
═══════════════════════════════════════════════════════════════════════
Microsoft.Maui.Essentials discovers platform implementations at runtime
through Android Context / Activity reflection (Clipboard, Preferences,
Vibration, Permissions, etc.). Stripping any implementation class breaks
the runtime lookup. -->
<assembly fullname="Microsoft.Maui.Essentials" preserve="all" />
<!-- ═══════════════════════════════════════════════════════════════════════
Utility libraries — cautiously preserved
═══════════════════════════════════════════════════════════════════════
These libraries are not deeply reflection-based, but they use patterns
(factory registries, codec detection, pluralisation rule scanning) that
are hard to fully trace statically. Preserving them is cheap (they are
all small) and eliminates the risk of a subtle runtime miss.
HtmlAgilityPack: XPath evaluator builds an internal function table from
HtmlAgilityPack.HtmlNode subtype names; stripping a node type would
silently return empty results from HTML parse paths in osu.Game.
Humanizer: pluralisation and date-difference rules are stored in nested
types discovered via Assembly.GetTypes() + attribute scan at first use.
TagLibSharp: codec/container detection uses a static factory list that
maps MIME types and file extensions to TagLib.File subclass instances
via Activator.CreateInstance(type) — stripping any File subclass causes
a TagLibNotSupportedException at audio-file import. -->
<assembly fullname="HtmlAgilityPack" preserve="all" />
<assembly fullname="Humanizer" preserve="all" />
<assembly fullname="TagLibSharp" preserve="all" />
<!--
Assemblies intentionally NOT listed here (will be tree-shaken by ILLink):
─────────────────────────────────────────────────────────────────────────
• NUnit — osu.Game embeds test-scene base classes referencing a small
subset of NUnit.Framework types. ILLink keeps exactly those types (e.g.
TestAttribute, TestFixtureAttribute) and strips the bulk of the assertion,
runner, constraint, and parallel-execution subsystems. The stripped types
are never invoked at runtime on a release Android build.
• DiffPlex — pure LCS diff algorithm; zero reflection; every type reachable
from osu.Game call sites is kept by the linker automatically.
• SharpCompress — stream-based archive/compression; all entry-point types
are accessed through factory static methods, not Type.GetType() lookups.
• Microsoft.Toolkit.HighPerformance — pure generics and unsafe extensions;
no runtime type lookup; the linker traces all references from osu.Game
correctly without a full-preservation override.
• All .NET BCL assemblies (System.*, mscorlib, netstandard) — ILLink follows
every reference from the preserved assemblies above and retains exactly the
BCL types that are used, discarding the rest.
-->
</linker>