Timezone data on Windows: tzdb.system cannot work there #4

Closed
opened 2026-08-31 10:07:33 -05:00 by jeff · 1 comment
Owner

tzdb.system cannot work on Windows. Every entry in search_directories
is a Unix path and default_localtime is /etc/localtime; they compile
there and fail at runtime, so a caller gets a file-not-found rather than
anything that explains itself.

Windows keeps its zone data in the registry under
HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones, as TZI
blobs plus Dynamic DST year-keyed rules, under Windows zone names such
as Central Standard Time. That is a different binary format with far
less history than IANA, and mapping the names either way needs CLDR's
windowsZones.xml.

-Dembed-tzdata already covers Windows completely: it needs no filesystem
and no allocator, and the data is the same IANA release everywhere.

Worth doing, cheapest first:

  1. Say so in the README. It currently names Windows in a single clause
    beside scratch containers and cross-compiled binaries
    (README.md:146), which undersells it: Windows is the only one of the
    three where the system path cannot work at all.

  2. Add a comptime system.available, false on Windows, and make the
    search list empty there. A caller then gets a signal it can branch on
    rather than a runtime failure on a path that never existed. The
    -Dno-system-tzdata machinery added in 9c9b5b7 is already this shape.

  3. Only if someone actually wants the machine's own zone on Windows:
    GetDynamicTimeZoneInformation, an embedded CLDR table to reach an
    IANA name, then embedded.load. That is a name lookup rather than a
    data source, and still requires -Dembed-tzdata for the data itself.

I would not write a registry reader. 1 and 2 are small and make the
current situation honest; 3 is a real project and only pays off for a
program that must follow the machine's own setting.

`tzdb.system` cannot work on Windows. Every entry in `search_directories` is a Unix path and `default_localtime` is `/etc/localtime`; they compile there and fail at runtime, so a caller gets a file-not-found rather than anything that explains itself. Windows keeps its zone data in the registry under `HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones`, as `TZI` blobs plus `Dynamic DST` year-keyed rules, under Windows zone names such as `Central Standard Time`. That is a different binary format with far less history than IANA, and mapping the names either way needs CLDR's `windowsZones.xml`. `-Dembed-tzdata` already covers Windows completely: it needs no filesystem and no allocator, and the data is the same IANA release everywhere. Worth doing, cheapest first: 1. Say so in the README. It currently names Windows in a single clause beside scratch containers and cross-compiled binaries (`README.md:146`), which undersells it: Windows is the only one of the three where the system path cannot work at all. 2. Add a comptime `system.available`, false on Windows, and make the search list empty there. A caller then gets a signal it can branch on rather than a runtime failure on a path that never existed. The `-Dno-system-tzdata` machinery added in 9c9b5b7 is already this shape. 3. Only if someone actually wants the machine's own zone on Windows: `GetDynamicTimeZoneInformation`, an embedded CLDR table to reach an IANA name, then `embedded.load`. That is a name lookup rather than a data source, and still requires `-Dembed-tzdata` for the data itself. I would not write a registry reader. 1 and 2 are small and make the current situation honest; 3 is a real project and only pays off for a program that must follow the machine's own setting.
jeff closed this issue 2026-08-31 17:44:18 -05:00
Author
Owner

Done in 71674f1, all three parts.

1 and 2. system.available is false on Windows, search_directories
is empty there, and load, loadLocal and version refuse with
error.NoSystemDatabase before touching the filesystem rather than
producing a file-not-found about a path that never existed. The README has
its own section now instead of the clause beside scratch containers.

3. tzdb.windows does the name lookup:
GetDynamicTimeZoneInformation for the invariant TimeZoneKeyName,
CLDR's table for the IANA name, embedded.load for the data. Each step is
public on its own, and ianaName is a plain table lookup that works on any
target. The registry is still not read, for the reason the issue gives.

The Win32 call is zigwin32, lazy and only when the target is Windows -- the
same shape zregex uses it in. CLDR's table is checked into the tree as
src/windowszones.zig rather than fetched: five kilobytes of names that
change about once a year, against a dependency and a network round trip in
front of every Windows build. zig build windowszones -Dwindowszones-xml=... regenerates it and reproduces the checked-in file
byte for byte.

What says it works. Wine, in a devShell of its own since nothing else
here needs it. CI cross-compiles the whole suite to x86_64-windows and
runs it under Wine, so the Win32 call really happens and the name really
gets looked up. I checked the tests bite by breaking each half in turn --
reading DaylightName instead of the key name, and making the table always
miss -- and both failed. End to end under Wine, for each machine zone
tried:

TZ                    TimeZoneKeyName                   IANA                jan     jul
America/Chicago       Central Standard Time             America/Chicago     -21600  -18000
Asia/Kolkata          India Standard Time               Asia/Calcutta        19800   19800
Europe/Berlin         W. Europe Standard Time           Europe/Berlin         3600    7200
Australia/Sydney      AUS Eastern Standard Time         Australia/Sydney     39600   36000
Pacific/Auckland      New Zealand Standard Time         Pacific/Auckland     46800   43200
America/Sao_Paulo     E. South America Standard Time    America/Sao_Paulo   -10800  -10800

A further test walks all 139 mappings and loads every one from the embedded
database, which is what would catch a name CLDR mapped in 2021a that IANA
has since dropped.

Two things fell out. zig build test -Dtarget=... used to fail on the
oracle steps, which link a host executable against the library; they now
take a host-built copy, which is the same code and the same answers. And
zig fmt --check . walks zigwin32's hundred thousand generated lines when
the package cache is inside the tree, so CI excludes zig-pkg.

Done in 71674f1, all three parts. **1 and 2.** `system.available` is false on Windows, `search_directories` is empty there, and `load`, `loadLocal` and `version` refuse with `error.NoSystemDatabase` before touching the filesystem rather than producing a file-not-found about a path that never existed. The README has its own section now instead of the clause beside scratch containers. **3.** `tzdb.windows` does the name lookup: `GetDynamicTimeZoneInformation` for the invariant `TimeZoneKeyName`, CLDR's table for the IANA name, `embedded.load` for the data. Each step is public on its own, and `ianaName` is a plain table lookup that works on any target. The registry is still not read, for the reason the issue gives. The Win32 call is zigwin32, lazy and only when the target is Windows -- the same shape zregex uses it in. CLDR's table is checked into the tree as `src/windowszones.zig` rather than fetched: five kilobytes of names that change about once a year, against a dependency and a network round trip in front of every Windows build. `zig build windowszones -Dwindowszones-xml=...` regenerates it and reproduces the checked-in file byte for byte. **What says it works.** Wine, in a devShell of its own since nothing else here needs it. CI cross-compiles the whole suite to `x86_64-windows` and runs it under Wine, so the Win32 call really happens and the name really gets looked up. I checked the tests bite by breaking each half in turn -- reading `DaylightName` instead of the key name, and making the table always miss -- and both failed. End to end under Wine, for each machine zone tried: TZ TimeZoneKeyName IANA jan jul America/Chicago Central Standard Time America/Chicago -21600 -18000 Asia/Kolkata India Standard Time Asia/Calcutta 19800 19800 Europe/Berlin W. Europe Standard Time Europe/Berlin 3600 7200 Australia/Sydney AUS Eastern Standard Time Australia/Sydney 39600 36000 Pacific/Auckland New Zealand Standard Time Pacific/Auckland 46800 43200 America/Sao_Paulo E. South America Standard Time America/Sao_Paulo -10800 -10800 A further test walks all 139 mappings and loads every one from the embedded database, which is what would catch a name CLDR mapped in 2021a that IANA has since dropped. Two things fell out. `zig build test -Dtarget=...` used to fail on the oracle steps, which link a host executable against the library; they now take a host-built copy, which is the same code and the same answers. And `zig fmt --check .` walks zigwin32's hundred thousand generated lines when the package cache is inside the tree, so CI excludes `zig-pkg`.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jeff/zig-datetime#4
No description provided.