ModernUO/Projects/Server
Kamron Batman 23dc6649a0
fix: Require only runtime packages on Linux, and check ICU and tzdata the way the runtime does (#2561)
## Why

ModernUO mandated `-dev` packages on production servers for exactly one reason: `DllImport` never
asks for a versioned SONAME, so `libdeflate.so.0` and `libargon2.so.1` sitting in `/usr/lib` went
unfound, and the `-dev` package's unversioned symlink was the only thing making resolution work.
The `-dev` packages ship no library of their own — operators were installing headers and a static
lib on machines that compile nothing.

Fixed in the binding packages (modernuo/LibDeflate.Bindings#4, modernuo/Argon2.Bindings#13), so
this picks them up and stops asking.

```
LibDeflate.Bindings 1.0.3  -> 1.0.4
Argon2.Bindings     1.17.0 -> 1.19.0
```

## zstd is dropped too, on every platform

ZstdNet bundles `libzstd` for `linux-x64`, `linux-arm64`, `osx-x64`, `osx-arm64` and win, and
nothing shells out to the CLI. Verified: the 15 `ManagedArchive` round-trip tests pass in a
container with no `zstd` package installed and `which zstd` empty. Removed from the README, the
macOS `brew install`, and CI — so the macOS runners now prove it rather than us assuming it.

## NativeLibraryChecker asks a different question

It asked *"is package X installed"* via `dpkg -l` / `rpm -q`. That is what forced `-dev`, and no
hardcoded name works for ICU anyway — its apt package is release-specific (`libicu70` on Ubuntu
22.04, `libicu76` on Debian 13). It now asks *"will the loader find this"*: `NativeLibrary.TryLoad`
on the unversioned name, then `libfoo.so.N` descending through the range the runtime accepts.

It deliberately does not consult a package database or `ldconfig -p`. Both answer a different
question than "will `dlopen` succeed" — see the ICU section below for how that bit.

## What was wrong with the ICU check

`libicuuc` was **inherited, not derived**. It came from translating the old package-name check into
a library probe, without establishing which library that should be. Reviewing it turned up three
defects, all of which could report ICU present on a host where the runtime then refuses to start:

- **`libicui18n` was never probed.** The only ICU names in `libSystem.Globalization.Native.so` are
  `libicuuc` and `libicui18n`. `libicudata` arrives as a dependency of `libicuuc`, and
  `libicuio`/`libicutu`/`libicutest` are never referenced — so that is the complete list, and both
  are checked now.
- **No version floor.** The runtime's `MinICUVersion` is 60, but the probe accepted down to
  `.so.0`. RHEL/CentOS 7 ships ICU 50, which passed and then aborted at startup.
- **The `ldconfig` fast path bypassed the range.** A cache line for `libicuuc.so.50` still matches a
  `libicuuc.so` prefix test, so the floor was unenforceable through it. It also trusts a stale
  cache — observed reporting a deleted `libdeflate` as present. Removed in favour of asking the
  loader directly, which reads the same cache but answers the real question, and which also deletes
  the musl special-case (`ldconfig -p` exits 0 on musl while producing nothing usable).

Worth knowing when this goes wrong in the field: **missing ICU does not throw, it `FailFast`s** —
SIGABRT, exit 134, uncatchable. The process starts cleanly and dies later at whatever line first
touches a culture, so the stack rarely implicates ICU.

## tzdata is a separate prerequisite, and nothing was checking it

The event scheduler resolves configured zone IDs through `TimeZoneInfo`, which reads
`/usr/share/zoneinfo`. It is data rather than a library, so no loader probe finds it, and slim
container images routinely omit it. Without it every lookup except `UTC` throws
`TimeZoneNotFoundException` and `GetSystemTimeZones()` returns 1 entry instead of ~419.

There is no per-zone packaging to opt into — it is ~2 MB for the whole set. The one split that does
exist is a trap rather than an optimization: Debian 12 and Ubuntu 24.04 move the legacy aliases into
`tzdata-legacy`, so plain `tzdata` has `America/New_York` and `EST5EDT` but is **missing
`US/Eastern` and `Asia/Calcutta`**. A shard configured with a legacy alias throws even though tzdata
is installed. Documented, with both fixes.

## Why `InvariantGlobalization` stays false

Dropping ICU entirely by turning on invariant mode looks tempting and is not safe. Because
`Directory.Build.props` also sets `PredefinedCulturesOnly=false`, invariant mode does **not** throw
`CultureNotFoundException` — it silently hands back invariant data. Measured on .NET 10:

| Behaviour | With ICU | Invariant mode |
|---|---|---|
| `new CultureInfo("de-DE")` | real culture | succeeds, returns invariant data |
| de-DE decimal separator | `,` | `.` |
| `1234.5` as de-DE | `1.234,5` | `1,234.5` |
| `string.Compare("a", "B", InvariantCulture)` | `-1` (linguistic) | `31` (ordinal) |
| sort `[b, A, a, B]` | `a, A, b, B` | `A, B, a, b` |
| `FindSystemTimeZoneById("Eastern Standard Time")` on Linux | resolves | `TimeZoneNotFoundException` |
| UTF-8 round-trip of non-ASCII | unaffected | unaffected |

Number parsing and formatting produce wrong values with no error, and culture-sensitive sort order
silently becomes ordinal. Encoding is not the mechanism — UTF-8 round-trips fine either way.

## Documentation

The rationale now lives in `dev-docs/platform-prerequisites.md` rather than in comments, so it is
discoverable without reading the build tool: what each dependency is for, what breaks without it,
per-distro package names, the ICU floor, the `tzdata-legacy` split, and why the check asks the
loader instead of the package manager.

README drops `libicu-dev`. Matching the runtime package by pattern (`'^libicu[0-9]+$'`) is
version-independent without pulling in headers, so **no `-dev` package is required on any supported
distribution** — which was the point of the whole change.

## CI now proves the claim instead of contradicting it

The dnf job already installed runtime packages only. The apt job installed `libicu-dev`, which ships
the unversioned `libicuuc.so` symlink — so every probe succeeded on the first attempt and the
versioned-SONAME fallback this PR depends on was never exercised. Switched to the pattern match,
verified to resolve exactly one package on jammy (70), bookworm (72), noble (74) and trixie (76).

Added an assertion that the unversioned symlinks are absent. Without it the suite silently stops
testing anything the moment a base image starts shipping one. Verified against all eight matrix
distributions — none ship them — and confirmed the step fails as intended when a symlink is planted.

## Audit of every other native entry point

Checked whether anything else has the same hazard. It does not:

| Import | Verdict |
|---|---|
| `ws2_32.dll` — `SocketHelper` | Always present on Windows |
| `libc` — `SocketHelper` | **Verified safe**, see below |
| ZstdNet → `libzstd` | Bundled for every RID |
| IORingGroup | No native library; raw syscalls |
| ICU | Loaded by the .NET runtime itself, which probes versioned suffixes |

`libc` deserved a hard look, because `libc.so` *is* a `libc6-dev` linker script while the real
library is `libc.so.6` — the same shape as the bug being fixed. It is not affected. Measured in a
container with no `libc6-dev`:

```
/usr/lib/x86_64-linux-gnu/libc.so   ABSENT
/lib/x86_64-linux-gnu/libc.so.6     present
TryLoad("libc")     LOADED      <- resolves where "libdeflate" would not
TryLoad("libc.so")  not found
getpid() -> DllImport("libc") WORKS
```

Confirmed on Alpine/musl as well. No code in this repo registers a `DllImportResolver`, and nothing
else P/Invokes.

## `--check-prereqs`

New flag. `Program.cs` only ran the SDK check in non-interactive mode — `NativeLibraryChecker` was
reachable only through the Spectre-driven guided flow, so there was no way to verify a deployment
target from a script or a container. It is what made the container verification below possible, and
it prints the exact ICU package for the running release via `apt-cache`.

It renders through the same `PrerequisiteChecker` the guided menu uses, rather than a second
hand-rolled table that could drift from it. Spectre drops ANSI styling on its own when stdout is not
a terminal, so redirected output stays clean; the console width is widened in that case so the
install hints, which are shell commands meant to be copied, do not gain a newline mid-command.

```
╭───────────────────────────╮
│ Checking native libraries │
╰───────────────────────────╯

  ✔ libicuuc (Found)
  ✔ libicui18n (Found)
   libdeflate (Not found)
   tzdata (Not found — every zone except UTC will throw)

  ⚠️ Install the missing dependencies. The -dev/-devel packages are not required:
   sudo apt-get install -y libicu74 libdeflate0 tzdata
```

Exit code carries the machine-readable half: 0 when everything resolves, 1 when anything is missing.

## Verification

Against 1.0.4 and 1.19.0: build plus **810 Server.Tests and 642 UOContent.Tests**, on Windows and
on Linux with **only** `libdeflate0` and `libargon2-1` installed — with the absence of the
unversioned symlink asserted first so the run could not pass for the wrong reason.

`--check-prereqs` verified in containers on Debian and Alpine across every state that matters: all
present, each dependency removed individually, tzdata removed, a deliberately stale `ldconfig`
cache, and ICU downgraded to `.so.50` to confirm the floor rejects it. Package resolution and the
absence of unversioned symlinks checked on all eight CI distributions.
2026-08-07 15:03:08 -07:00
..
Buffers feat(buffers): add :L lowercase format spec to RawInterpolatedStringHandler (#2440) 2026-05-03 18:24:57 -07:00
Client feat: Add zero-alloc interpolation handler to ValueStringBuilder, replace all StringBuilder usage (#2387) 2026-03-22 14:23:44 -07:00
Collections feat(network): pluggable connection filters; file blocklist + contribute-first CrowdSec (#2542) 2026-07-25 11:59:37 -07:00
Comparers fix: Bumps deps. Updates copyrights (#2353) 2026-03-05 19:36:54 -08:00
Compression feat(pathfinding): .swb format v8 compact index (#3b) (#2471) 2026-06-07 01:22:20 -07:00
Configuration fix(console): stop headless servers from pegging a CPU core (#2535) 2026-07-16 18:52:43 -07:00
Console fix(console): stop headless servers from pegging a CPU core (#2535) 2026-07-16 18:52:43 -07:00
ContextMenus fix: Bumps deps. Updates copyrights (#2353) 2026-03-05 19:36:54 -08:00
Events fix: Bumps deps. Updates copyrights (#2353) 2026-03-05 19:36:54 -08:00
Exceptions fix: Bumps deps. Updates copyrights (#2353) 2026-03-05 19:36:54 -08:00
GarbageCollection chore: Use var everywhere (#2294) 2025-12-27 16:47:28 -08:00
Geometry fix: Bumps deps. Updates copyrights (#2353) 2026-03-05 19:36:54 -08:00
Items fix(opl): refuse property list invalidation raised from inside GetProperties (#2555) 2026-07-28 21:29:03 -07:00
Json fix(json): Rectangle3DConverter loses a z-level on write (#2506) 2026-06-25 23:42:49 -07:00
Localization feat: Add zero-alloc interpolation handler to ValueStringBuilder, replace all StringBuilder usage (#2387) 2026-03-22 14:23:44 -07:00
Maps fix: Fixes pathfinding prebake and pathfinding multi-fallthrough. (#2478) 2026-06-08 11:59:24 -07:00
Menus feat: Pre-Publish 14 Crafting (supersedes #2181, #2381) (#2476) 2026-06-07 20:27:22 -07:00
Migrations refactor: decompose mobile/corpse hair, delete VirtualHairInfo, fix removal serial (#2462) (#2463) 2026-06-06 14:33:28 -07:00
Mobiles fix(opl): refuse property list invalidation raised from inside GetProperties (#2555) 2026-07-28 21:29:03 -07:00
Network feat(network): allowlist false-positive IPs, escalate on behavior (#2556) 2026-07-30 23:12:17 -07:00
PropertyList fix(opl): refuse property list invalidation raised from inside GetProperties (#2555) 2026-07-28 21:29:03 -07:00
Random fix: Bumps deps. Updates copyrights (#2353) 2026-03-05 19:36:54 -08:00
Regions fix: Bumps deps. Updates copyrights (#2353) 2026-03-05 19:36:54 -08:00
Serialization perf(saves): eliminate world-save freeze bottlenecks (~9.5x faster freeze) (#2525) 2026-07-16 22:53:28 -07:00
Targeting chore: Use var everywhere (#2294) 2025-12-27 16:47:28 -08:00
Text feat: Add zero-alloc interpolation handler to ValueStringBuilder, replace all StringBuilder usage (#2387) 2026-03-22 14:23:44 -07:00
TileMatrix feat(pathfinding): JSONL recorder + public bake helpers (#2449) 2026-05-06 13:06:28 -07:00
Timer feat: Adds dynamic thread idle to address CPU usage (#2370) 2026-03-13 23:53:49 -07:00
TimeZones fix: Bumps deps. Updates copyrights (#2353) 2026-03-05 19:36:54 -08:00
Utilities feat(network): pluggable connection filters; file blocklist + contribute-first CrowdSec (#2542) 2026-07-25 11:59:37 -07:00
World perf(saves): eliminate world-save freeze bottlenecks (~9.5x faster freeze) (#2525) 2026-07-16 22:53:28 -07:00
AssemblyHandler.cs fix: Bumps deps. Updates copyrights (#2353) 2026-03-05 19:36:54 -08:00
Attributes.cs fix: Bumps deps. Updates copyrights (#2353) 2026-03-05 19:36:54 -08:00
CityInfo.cs fix: Bumps deps. Updates copyrights (#2353) 2026-03-05 19:36:54 -08:00
Commands.cs fix: Bumps deps. Updates copyrights (#2353) 2026-03-05 19:36:54 -08:00
Effects.cs fix: Bumps deps. Updates copyrights (#2353) 2026-03-05 19:36:54 -08:00
EntityFinalizationTracker.cs fix: Bumps deps. Updates copyrights (#2353) 2026-03-05 19:36:54 -08:00
EventLoopTasks.cs fix: Bumps deps. Updates copyrights (#2353) 2026-03-05 19:36:54 -08:00
ExpansionInfo.cs fix: Bumps deps. Updates copyrights (#2353) 2026-03-05 19:36:54 -08:00
FeatureFlags.cs fix: Streamlines insurance. Insurance only executes when enabled. (#2550) 2026-07-26 09:47:49 -07:00
Guild.cs perf(saves): eliminate world-save freeze bottlenecks (~9.5x faster freeze) (#2525) 2026-07-16 22:53:28 -07:00
HuePicker.cs fix: Cleans up core code (#1187) 2022-10-10 21:47:08 -07:00
IAccount.cs fix: Fixes looking up accounts that were renamed. (#2078) 2025-01-18 17:11:43 -08:00
IEntity.cs perf(saves): eliminate world-save freeze bottlenecks (~9.5x faster freeze) (#2525) 2026-07-16 22:53:28 -07:00
Interfaces.cs feat: Add CanSpawnMobile overload with props Z-range support. (#2293) 2025-12-27 17:01:15 -08:00
IVirtualCheckGump.cs feat: Moves gumps out of the core (#1916) 2024-08-09 19:07:32 -07:00
KeywordList.cs fix: Cleans up core code (#1187) 2022-10-10 21:47:08 -07:00
Main.cs feat(network): pluggable connection filters; file blocklist + contribute-first CrowdSec (#2542) 2026-07-25 11:59:37 -07:00
MessageType.cs fix: Bumps deps. Updates copyrights (#2353) 2026-03-05 19:36:54 -08:00
Module.cs fix: Fixes sending packets and sidesteps a major issue with stackalloc and PGO in .NET 8 (#1607) 2023-11-21 12:18:20 -08:00
Party.cs fix: Cleans up core code (#1187) 2022-10-10 21:47:08 -07:00
Poison.cs feat: Refactor Poison system, implement Darkglow & Parasitic effects (#2385) 2026-03-21 21:27:22 -07:00
Prompt.cs fix: Cleans up core code (#1187) 2022-10-10 21:47:08 -07:00
Race.cs fix: Fixes TryParse for Race (#1244) 2022-11-12 00:31:25 -08:00
SecureTrade.cs fix: Bumps deps. Updates copyrights (#2353) 2026-03-05 19:36:54 -08:00
Serial.cs fix: Bumps deps. Updates copyrights (#2353) 2026-03-05 19:36:54 -08:00
Server.csproj fix: Require only runtime packages on Linux, and check ICU and tzdata the way the runtime does (#2561) 2026-08-07 15:03:08 -07:00
Skills.cs chore: Use var everywhere (#2294) 2025-12-27 16:47:28 -08:00
TileData.cs fix: Bumps deps. Updates copyrights (#2353) 2026-03-05 19:36:54 -08:00
TileList.cs fix: Bumps deps. Updates copyrights (#2353) 2026-03-05 19:36:54 -08:00