OS-level dependencies ModernUO needs at runtime, why each one is required, and what breaks without it. This page is about software packages, not hardware sizing.
Run ./build-tool --check-prereqs from the repository root to check the current machine. It prints
the exact install command for the detected distribution.
| Dependency | Platform | Why |
|---|---|---|
| .NET 10 Runtime | all | — |
ICU (libicuuc, libicui18n) |
Linux, macOS | The runtime refuses to start without it; see below |
| tzdata | Linux | Time zone lookups; see below |
libdeflate |
all | LibDeflate.Bindings |
libargon2 |
all | Argon2.Bindings (password hashing) |
| VC++ Redistributable v14 | Windows | Native bindings |
Not required, despite appearances:
- zstd —
ZstdNetbundleslibzstdfor every RID. - liburing —
IORingGroupissuesio_uringsyscalls directly. It imports onlylibc,libSystem.dylib,kernel32.dll,kernelbase.dllandws2_32.dll. -dev/-develpackages — see "Runtime packages only" below.
# Debian / Ubuntu (ICU has no stable package alias, so match it by pattern)
sudo apt-get install -y '^libicu[0-9]+$' libdeflate0 libargon2-1 tzdata
# Fedora / RHEL
sudo dnf install -y libdeflate libargon2 libicu tzdata
# Alpine
apk add --no-cache libdeflate argon2-libs icu-libs tzdata
# macOS
brew install icu4c libdeflate argon2CentOS additionally needs EPEL and CRB:
sudo dnf install -y epel-release epel-next-release && sudo dnf config-manager --set-enabled crbOnly the runtime packages are needed. The -dev/-devel packages are not required.
They used to be, because .NET's DllImport probing looks for the unversioned libfoo.so, and on
Linux that bare symlink ships only in the development package. The runtime package ships the
versioned SONAME (libdeflate.so.0, libargon2.so.1). The binding packages now probe the versioned
names as well, so the runtime package is sufficient.
Anything still documenting libicu-dev or libdeflate-dev as a requirement is out of date.
Directory.Build.props sets InvariantGlobalization=false, so ICU is mandatory. Without it the
runtime does not throw — it FailFasts:
Couldn't find a valid ICU package installed on the system. Please install libicu (or icu-libs)
using your package manager and try again.
That is SIGABRT (exit 134) and it cannot be caught. Note the process starts cleanly and aborts
later, at whatever line first touches a culture, so the crash rarely points at the cause.
InvariantGlobalization=true would remove the ICU dependency, but it changes behaviour in ways that
corrupt data silently. Measured on .NET 10 with the repository's settings:
| 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 |
The dangerous row is the first. Because Directory.Build.props also sets
PredefinedCulturesOnly=false, constructing a culture in invariant mode succeeds instead of
throwing CultureNotFoundException, and hands back an object populated with invariant data. Number
parsing and formatting then produce wrong values with no error, and culture-sensitive sort order
silently becomes ordinal.
Encoding is not affected — UTF-8 round-trips correctly in both modes.
The runtime accepts libicuuc.so.60 and above (MinICUVersion in pal_icushim.c). The prerequisite
checker enforces the same floor, so a host carrying only an older ICU is reported missing rather than
passing and then aborting at startup. RHEL/CentOS 7 ships ICU 50 and is affected.
ICU tracks its own release train, so the SONAME digit varies widely by distribution — .so.74 on
Ubuntu 24.04, .so.76 on Alpine, .so.77 on Fedora, .so.78 on openSUSE. There is no stable
package alias on Debian and Ubuntu, which is why the checker resolves the name via apt-cache
instead of hardcoding one.
Only libicuuc and libicui18n are used; those are the two names
libSystem.Globalization.Native.so loads. libicudata arrives as a dependency of libicuuc, and
libicuio/libicutu/libicutest are never referenced. Every distribution ships all of them in a
single package, so installing ICU at all satisfies both.
The event scheduler resolves configured zone IDs through TimeZoneInfo, which reads
/usr/share/zoneinfo on Linux. This is separate from ICU: it is data, not a library, so no loader
probe finds it, and slim container images routinely omit it.
Without tzdata every lookup except UTC throws:
TimeZoneNotFoundException: The time zone ID 'America/New_York' was not found on the local computer.
TimeZoneInfo.GetSystemTimeZones() returns 1 entry instead of ~419, and TimeZoneInfo.Local falls
back to UTC.
Distributions do not package individual zones — it is one tzdata package, about 2 MB installed for
the full set. Subsetting is not worth pursuing.
The one split that does exist is tzdata-legacy on Debian 12 and Ubuntu 24.04, which carries the
deprecated aliases. With plain tzdata alone:
| Zone ID | tzdata |
tzdata-legacy |
|---|---|---|
America/New_York |
present | — |
Europe/Kyiv |
present | — |
EST5EDT |
present | — |
US/Eastern |
missing | present |
Asia/Calcutta |
missing | present |
So a shard configured with a legacy alias such as US/Eastern throws on a current Debian or Ubuntu
even though tzdata is installed. Either install tzdata-legacy or switch the configured value to the
canonical ID (America/New_York, Asia/Kolkata).
TZDIR is honoured if the data lives somewhere non-standard.
--check-prereqs asks the loader directly — NativeLibrary.TryLoad on the unversioned name, then
libfoo.so.N descending through the accepted range.
It deliberately does not consult a package database or ldconfig -p. Both answer a different
question than "will dlopen succeed":
- Package queries need a hardcoded name, which does not exist for ICU.
ldconfig's cache can be stale, omitsLD_LIBRARY_PATH, and carries no version information to enforce the ICU floor against. On musl it exits successfully while producing nothing usable.