nandi/frqpublic Fork 0
bffe879
Commits
Clone
git clone https://git.rickub.com/nandi/frq.git
git clone ssh://git@rickub.com/nandi/frq.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Read the clock in the reader's zone when /etc/localtime is a file

Every timestamp came out in UTC under the nix package: eight in the
morning next to a clock reading one. The zone is found by resolving
/etc/localtime and taking the name out of the path, and in a container
that file is often the bytes themselves rather than a symlink — Arch
under distrobox has it as an overlay mount — so readlink answers with
the path it was given, no name comes out, and the chain fell through to
UTC. libc knew the zone throughout; only the name recovery failed.

So fall back to libc's colon form, TZ=:/etc/localtime, which names the
tzfile by path instead of by zone and reads it with its transitions
intact: -25200 in September and -28800 in February, not one frozen
offset. Nothing downstream minds that it is a path, the zone only ever
reaching tz-offset-seconds.

TZDIR on the jolt wrapper for the other half of the same question: a
zone *name* needs a zoneinfo tree to resolve against, and a NixOS host
has no /usr/share/zoneinfo. Set as a default, so one already set wins.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-09-01T01:18:04-07:00 Browse files
bffe879 parent: 7434fee
added CLAUDE.md +20 -0
new file mode 100644
@@ -0,0 +1,20 @@
1+# Working in this repo
2+
3+## Nix
4+
5+`nix` is not installed on the host. It lives in the Arch distrobox, so run every
6+nix command through that:
7+
8+```bash
9+distrobox enter arch -- bash -lc 'cd <this directory> && nix build .#frq'
10+```
11+
12+The worktree path is the same inside the container as outside, so `cd "$PWD"`
13+works. A remote builder (`eu.nixbuild.net`) is already configured there; the
14+Android outputs want `--store ssh-ng://eu.nixbuild.net --eval-store auto`, for
15+the reason nix/android.nix gives at the top.
16+
17+One thing that container is *not* representative of: `/etc/localtime` is a
18+regular file there rather than a symlink, so anything that reads the zone out
19+of its path sees nothing. That is a real deployment shape, not an artefact —
20+frq.clock handles it.
new file mode 100644
@@ -0,0 +1,20 @@
1+# Working in this repo
2+
3+## Nix
4+
5+`nix` is not installed on the host. It lives in the Arch distrobox, so run every
6+nix command through that:
7+
8+```bash
9+distrobox enter arch -- bash -lc 'cd <this directory> && nix build .#frq'
10+```
11+
12+The worktree path is the same inside the container as outside, so `cd "$PWD"`
13+works. A remote builder (`eu.nixbuild.net`) is already configured there; the
14+Android outputs want `--store ssh-ng://eu.nixbuild.net --eval-store auto`, for
15+the reason nix/android.nix gives at the top.
16+
17+One thing that container is *not* representative of: `/etc/localtime` is a
18+regular file there rather than a symlink, so anything that reads the zone out
19+of its path sees nothing. That is a real deployment shape, not an artefact —
20+frq.clock handles it.
modified flake.nix +7 -0
@@ -165,10 +165,17 @@
165165
166166 # jolt.deps shells out to git and unzip, and jolt.mvn-http dlopens
167167 # OpenSSL through the JOLT_OPENSSL_LIBDIR seam.
168+ #
169+ # TZDIR so a zone *name* resolves wherever this runs: frq.clock
170+ # hands one to tzset, and glibc then looks for the tzfile under
171+ # /usr/share/zoneinfo unless told otherwise — which a NixOS host
172+ # does not have. The store's own tzdata is there on both kinds of
173+ # machine. --set-default, so a TZDIR the user set still wins.
168174 postFixup = ''
169175 wrapProgram "$out/bin/jolt" \
170176 --prefix PATH : "${pkgs.lib.makeBinPath [ pkgs.git pkgs.unzip ]}" \
171177 --set-default JOLT_OPENSSL_LIBDIR "${pkgs.lib.makeLibraryPath [ pkgs.openssl ]}" \
178+ --set-default TZDIR "${pkgs.tzdata}/share/zoneinfo" \
172179 --set-default SSL_CERT_FILE "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"
173180 '';
174181 };
@@ -165,10 +165,17 @@
165 165
166 # jolt.deps shells out to git and unzip, and jolt.mvn-http dlopens166 # jolt.deps shells out to git and unzip, and jolt.mvn-http dlopens
167 # OpenSSL through the JOLT_OPENSSL_LIBDIR seam.167 # OpenSSL through the JOLT_OPENSSL_LIBDIR seam.
168+ #
169+ # TZDIR so a zone *name* resolves wherever this runs: frq.clock
170+ # hands one to tzset, and glibc then looks for the tzfile under
171+ # /usr/share/zoneinfo unless told otherwise — which a NixOS host
172+ # does not have. The store's own tzdata is there on both kinds of
173+ # machine. --set-default, so a TZDIR the user set still wins.
168 postFixup = ''174 postFixup = ''
169 wrapProgram "$out/bin/jolt" \175 wrapProgram "$out/bin/jolt" \
170 --prefix PATH : "${pkgs.lib.makeBinPath [ pkgs.git pkgs.unzip ]}" \176 --prefix PATH : "${pkgs.lib.makeBinPath [ pkgs.git pkgs.unzip ]}" \
171 --set-default JOLT_OPENSSL_LIBDIR "${pkgs.lib.makeLibraryPath [ pkgs.openssl ]}" \177 --set-default JOLT_OPENSSL_LIBDIR "${pkgs.lib.makeLibraryPath [ pkgs.openssl ]}" \
178+ --set-default TZDIR "${pkgs.tzdata}/share/zoneinfo" \
172 --set-default SSL_CERT_FILE "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"179 --set-default SSL_CERT_FILE "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"
173 '';180 '';
174 };181 };
modified src/frq/clock.jolt +13 -0
@@ -12,12 +12,25 @@
1212 (def ^:private zone
1313 ;; TZ if it is set, otherwise whatever /etc/localtime points at, otherwise
1414 ;; UTC — which is wrong by hours but never wrong by a day's worth of parsing.
15+ ;;
16+ ;; /etc/localtime is not always a symlink. In a container it is often the
17+ ;; bytes themselves — distrobox's Arch has it as an overlay-mounted regular
18+ ;; file — and then `readlink -f` answers with the path it was given and there
19+ ;; is no zone name to read out of it at all. That fell through to UTC, so a
20+ ;; channel read seven hours ahead of the clock on the same screen.
21+ ;;
22+ ;; The colon form is what libc has for exactly this: `TZ=:/etc/localtime`
23+ ;; names the tzfile by path rather than by zone, and tzset reads it with the
24+ ;; transitions intact — the offset it gives is DST-correct per instant, not a
25+ ;; fixed one. Nothing downstream cares that this is a path: the zone is only
26+ ;; ever handed back to tz-offset-seconds, never shown.
1527 (delay
1628 (or (let [tz (host/getenv "TZ")] (when (seq tz) tz))
1729 (try
1830 (second (re-find #"/zoneinfo/(.+)$"
1931 (str/trim (str (host/sh-out "readlink -f /etc/localtime")))))
2032 (catch Exception _ nil))
33+ (when (host/file-exists? "/etc/localtime") ":/etc/localtime")
2134 "UTC")))
2235
2336 (defn now-ms [] (quot (host/wall-nanos) 1000000))
@@ -12,12 +12,25 @@
12 (def ^:private zone12 (def ^:private zone
13 ;; TZ if it is set, otherwise whatever /etc/localtime points at, otherwise13 ;; TZ if it is set, otherwise whatever /etc/localtime points at, otherwise
14 ;; UTC — which is wrong by hours but never wrong by a day's worth of parsing.14 ;; UTC — which is wrong by hours but never wrong by a day's worth of parsing.
15+ ;;
16+ ;; /etc/localtime is not always a symlink. In a container it is often the
17+ ;; bytes themselves — distrobox's Arch has it as an overlay-mounted regular
18+ ;; file — and then `readlink -f` answers with the path it was given and there
19+ ;; is no zone name to read out of it at all. That fell through to UTC, so a
20+ ;; channel read seven hours ahead of the clock on the same screen.
21+ ;;
22+ ;; The colon form is what libc has for exactly this: `TZ=:/etc/localtime`
23+ ;; names the tzfile by path rather than by zone, and tzset reads it with the
24+ ;; transitions intact — the offset it gives is DST-correct per instant, not a
25+ ;; fixed one. Nothing downstream cares that this is a path: the zone is only
26+ ;; ever handed back to tz-offset-seconds, never shown.
15 (delay27 (delay
16 (or (let [tz (host/getenv "TZ")] (when (seq tz) tz))28 (or (let [tz (host/getenv "TZ")] (when (seq tz) tz))
17 (try29 (try
18 (second (re-find #"/zoneinfo/(.+)$"30 (second (re-find #"/zoneinfo/(.+)$"
19 (str/trim (str (host/sh-out "readlink -f /etc/localtime")))))31 (str/trim (str (host/sh-out "readlink -f /etc/localtime")))))
20 (catch Exception _ nil))32 (catch Exception _ nil))
33+ (when (host/file-exists? "/etc/localtime") ":/etc/localtime")
21 "UTC")))34 "UTC")))
22 35
23 (defn now-ms [] (quot (host/wall-nanos) 1000000))36 (defn now-ms [] (quot (host/wall-nanos) 1000000))