From 9f413f706320c6ca1fadf9c189a253dc65a3e224 Mon Sep 17 00:00:00 2001 From: sepehr-safari Date: Mon, 7 Sep 2026 23:40:33 +0300 Subject: [PATCH] The checks that exist actually run Six things, all of them a guard that was written and then did not fire. **The GTK check never ran on Debian.** It was wrapped in `command -v ldconfig`, and Debian keeps `ldconfig` in /usr/sbin, which it does not put on a normal user's PATH. So on one of the three distributions this project names as supported, the check that stops you downloading an app your machine cannot run silently did not happen: a verified download, "Installed Notary", and then nothing when it starts. Reproduced in a clean debian:13 container, fixed, and reproduced again as a clean refusal. It looks for ldconfig by absolute path now, and searches the library directories if there is none at all, and reports "cannot tell" rather than guessing. **The floor is checked before GTK presence.** An Ubuntu 22.04 machine has GTK 4.6, so a presence check passes there and then tells the reader nothing, while the floor is the actual reason their machine cannot run this. **The SHA-256 check installed anyway when the digest could not be fetched.** A verification step that any bad minute switches off is not one. It also requires both digests to be non-empty before comparing: two empty strings compare equal and the check then reports success over nothing at all. I hit exactly that bug in my own verification script this week, which is why it is guarded here. **A first start that fails now says so.** The app was launched with its output thrown away, so a window that died on a missing library was indistinguishable from one that opened behind something. On the app that holds your key, that silence is the worst available failure mode. **A second Notary on one key said nothing at all.** The daemon takes an exclusive lock on the key file and answers the second process 409 with "another Notary already has this key open". The window threw that away and re-synced, which lands back on the unlock screen with no error: the right passphrase, the spinner stops, nothing happens, forever. Two ordinary paths reach it, installing Plaza and then Notary, and re-running the installer while Notary is open. It distinguishes the two things 409 means now, and a daemon test pins the wording the window matches on, because the two binaries cannot import each other. Probed: changing that wording fails the test by name. **Two message buffers were too small for their messages.** `setExitNote` formats with `catch return`, so a note one byte too long was not truncated, it was dropped: a dead window with nothing at all where the explanation goes. Also: "This Mac holds the only copy" beside the backup button now says "this computer". 0.10.7's notes claimed that sweep covered everywhere it appeared. It had missed the one sentence that tells a Linux reader why losing this machine loses their identity. A failed signer no longer says "check SIGNER_BIN", a developer override documented only in gui/README.md that nobody who used the installer has ever set. `tar` failures, unknown arguments and `--help` produce messages and the right exit codes; the EXIT trap was returning the status of its own failed test, so `--help` exited 1. And two empty files named `no-such-key.ncryptsec` are deleted. A test opens that path expecting it to be absent and was reading the working directory, so the file it is named for existed. It uses its own temp directory now. Notary is 0.10.10. --- .github/RELEASE_NOTES.md | 16 ++++ CHANGELOG.md | 54 +++++++++++++ daemon/no-such-key.ncryptsec | 0 daemon/src/approval_http.zig | 49 +++++++++++- daemon/src/onboarding.zig | 2 +- gui/app.zon | 2 +- gui/src/app.native | 2 +- gui/src/main.zig | 45 +++++++++-- no-such-key.ncryptsec | 0 scripts/install-linux.sh | 143 ++++++++++++++++++++++++++--------- 10 files changed, 265 insertions(+), 48 deletions(-) delete mode 100644 daemon/no-such-key.ncryptsec delete mode 100644 no-such-key.ncryptsec diff --git a/.github/RELEASE_NOTES.md b/.github/RELEASE_NOTES.md index 3555ebb..84bb8f0 100644 --- a/.github/RELEASE_NOTES.md +++ b/.github/RELEASE_NOTES.md @@ -1,5 +1,21 @@ **Notary**: a native NIP-46 remote signer for Nostr. macOS (Apple Silicon), **ad-hoc signed (not notarized)**, and Linux (x86_64 and aarch64). +### What's new in v0.10.10 + +**Fixed: on Debian the installer never checked for GTK 4.** The check that stops you downloading an app your machine cannot run was gated on finding `ldconfig` on your PATH, and Debian keeps `ldconfig` in `/usr/sbin`, which it does not put on a normal user's PATH. So on Debian the check silently did not happen: a machine without GTK 4 got a verified download, "Installed Notary", and then nothing at all when it started. It looks for `ldconfig` by absolute path now, and searches the library directories if there is none. + +**A first start that fails now says so.** Notary was launched at the end of the install with its output thrown away, so a window that died on a missing library was indistinguishable from one that opened behind something. If it exits immediately the installer prints what it said. On the app that holds your key, that silence was the wrong trade. + +**The download is verified, or not installed.** If the published SHA-256 could not be fetched, the installer used to warn and install anyway, which is a check any bad minute switches off. + +**"Another Notary already has this key open" is now something you can read.** Two Notary windows cannot share one key: the daemon takes an exclusive lock on the key file and answers the second one with a refusal. The second window threw that refusal away, so you typed the right passphrase, the spinner stopped, and nothing happened, with nothing anywhere saying why. It happens in ordinary use: installing Plaza and then Notary, or re-running the installer while Notary is open. + +**"This Mac" really does read "this computer" now.** v0.10.7 said that sweep was done everywhere. It had missed the sentence next to the backup button, which is the one place a Linux user is told why losing this machine loses their identity. + +**A dead signer no longer tells you to check `SIGNER_BIN`.** That is a developer override nobody who used the one-line installer has ever set. It names the actual problem instead, which is that the two binaries have to sit together. + +To be explicit about something v0.10.7 left ambiguous: that release fixed a **compile** error on older systems, and Ubuntu 22.04 and Debian 12 still cannot run these downloads. They carry GTK 4.6 and this needs 4.10. Building from source on them works if their GTK is new enough. + ### What's new in v0.10.9 **Housekeeping, and one leak.** When a relay connection has gone quiet and what to do about it now comes from the `nostr` library rather than a copy kept here: the same three numbers and the same decision existed in Notary and in Plaza, written down in neither, so a correction to one would silently not reach the other. diff --git a/CHANGELOG.md b/CHANGELOG.md index 41142a9..d4e83c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,60 @@ While pre-1.0, minor versions add capability and patch versions are fixes. ## [Unreleased] +## [0.10.10] - 2026-09-07 + +### Fixed + +- The Linux installer's GTK 4 check was skipped entirely on Debian. It was + gated on `command -v ldconfig`, and Debian does not put `/usr/sbin` on a + normal user's PATH, so on one of the three distributions this project names + as supported the check silently did not run: a machine without GTK 4 got a + verified download, a cheerful "Installed Notary", and an app that died on + `libgtk-4.so.1`. It looks for `ldconfig` by absolute path too, and falls back + to searching the library directories. + +- The installer launched Notary with its output discarded, so a first run that + failed looked exactly like one that succeeded. It reports what the app said + if it exits immediately. + +- The SHA-256 check installed anyway when the published digest could not be + fetched. It now refuses, and it requires both digests to be non-empty before + comparing them: two empty strings compare equal, and the check then reports + success over nothing at all. + +- The distribution floor is now checked before GTK presence. An Ubuntu 22.04 + machine has GTK 4.6, so a presence check passes there and says nothing useful + while the floor is the actual reason it cannot run this. + +- A second Notary on the same key left the unlock screen silent. The daemon + answers 409 with "another Notary already has this key open", and the window + discarded it and re-synced, so the right passphrase produced no error and no + progress. Two ordinary paths reach it: installing Plaza and then Notary, and + re-running the installer while Notary is open. + +- `exit_note_buf` and `onboard_error_buf` were too small for the messages + written into them. `setExitNote` formats with `catch return`, so a message one + byte too long was not truncated but dropped entirely, leaving a dead window + with no explanation at all. + +- "This Mac holds the only copy" next to the backup button now says "this + computer". 0.10.7 claimed that sweep covered everywhere it appeared; it had + missed the one sentence a Linux user reads about losing their identity. + +- A signer that fails to start no longer tells the reader to check `SIGNER_BIN`, + a developer override documented only in `gui/README.md`. + +- `tar` failures, unknown arguments and `--help` now produce messages and the + right exit codes. The `EXIT` trap returned the status of its own failed test + when no temp directory had been made, so `--help` exited 1. + +### Removed + +- Two committed empty files named `no-such-key.ncryptsec`, at the repository + root and in `daemon/`. A test opens that path expecting it to be absent, and + it was reading the working directory, so the file the test is named for + existed. The test now uses its own temp directory. + ## [0.10.9] - 2026-09-07 ### Changed diff --git a/daemon/no-such-key.ncryptsec b/daemon/no-such-key.ncryptsec deleted file mode 100644 index e69de29..0000000 diff --git a/daemon/src/approval_http.zig b/daemon/src/approval_http.zig index 3940f5c..d6b2594 100644 --- a/daemon/src/approval_http.zig +++ b/daemon/src/approval_http.zig @@ -34,6 +34,7 @@ const ipc = nostr.signer_ipc; const Broker = approval.Broker; const Pending = approval.Pending; const Gate = onboarding.Gate; +const keystore = nostr.keystore; /// Live connection state of one relay, reported per relay on `/info`. /// A relay connection's live state. @@ -748,7 +749,7 @@ fn handleSetup(self: *Server, io: std.Io, w: *std.Io.Writer, body: []const u8) ! } /// What `/forget` requires in its body before it will delete anything. Typed by -/// the reader, not clicked: this removes the only copy of a key on this Mac. +/// the reader, not clicked: this removes the only copy of a key on this machine. pub const forget_confirmation = "delete my key"; /// POST /forget, remove the key file so another account can be set up. @@ -954,7 +955,7 @@ fn handleUnlock(self: *Server, io: std.Io, w: *std.Io.Writer, body: []const u8) error.BadPassphrase => respond(w, 401, "{\"error\":\"bad passphrase\"}"), error.NotLocked => respond(w, 409, "{\"error\":\"not locked\"}"), // Not the reader's mistake, and not something a passphrase fixes. - // Another Notary on this Mac has this key open; two processes + // Another Notary on this machine has this key open; two processes // cannot share a decrypted key, so one of them has to close. error.AlreadyOpenElsewhere => respond(w, 409, "{\"error\":\"another Notary already has this key open\"}"), else => respond(w, 500, "{\"error\":\"could not unlock\"}"), @@ -2174,7 +2175,12 @@ test "a wrong passphrase is written down, because a run of them is the only warn var log = audit.Log{ .dir = tmp.dir, .path = "audit.log" }; var broker: Broker = .{}; - var gate = Gate.init(gpa, std.Io.Dir.cwd(), "no-such-key.ncryptsec", .locked); + // The tmp dir, not the CWD. Pointed at the working directory this test read + // "no-such-key.ncryptsec" relative to wherever it happened to run, and two + // empty files of that name had been committed at the top of this repo and + // inside daemon/, so the file the test is named for existed. They are gone, + // and this no longer depends on that. + var gate = Gate.init(gpa, tmp.dir, "no-such-key.ncryptsec", .locked); var server = Server{ .gpa = gpa, .broker = &broker, .gate = &gate, .token = "t", .log = &log, .info = .{ .relays = &.{}, .timeout_ms = 1000 }, .host = "127.0.0.1", .port = 0 }; var out = std.Io.Writer.Allocating.init(gpa); @@ -2188,6 +2194,43 @@ test "a wrong passphrase is written down, because a run of them is the only warn try testing.expect(std.mem.indexOf(u8, written, "hunter2") == null); } +test "a second Notary is told WHY, in the words the window matches on" { + // The window reads this body. It shows its own sentence when it finds + // "already has this key open" in a 409, and re-syncs silently otherwise, + // because 409 also means the benign "initialized out from under us". So the + // wording here is a contract between two binaries that cannot import each + // other, and changing it silently returns the window to the failure this + // fixed: the right passphrase, the spinner stopping, and nothing happening. + const gpa = testing.allocator; + const io = testing.io; + var tmp = testing.tmpDir(.{}); + defer tmp.cleanup(); + + const secret = [_]u8{0xC2} ** 32; + const passphrase = "correct horse battery staple"; + const ncryptsec = try keystore.encryptKey(gpa, io, secret, passphrase, .known_secure); + defer gpa.free(ncryptsec); + try keystore.writeNewKeyFile(io, tmp.dir, "key.ncryptsec", ncryptsec); + + var holder = Gate.init(gpa, tmp.dir, "key.ncryptsec", .locked); + try holder.unlock(io, passphrase); + + var log = audit.Log{ .dir = tmp.dir, .path = "audit.log" }; + var broker: Broker = .{}; + var gate = Gate.init(gpa, tmp.dir, "key.ncryptsec", .locked); + var server = Server{ .gpa = gpa, .broker = &broker, .gate = &gate, .token = "t", .log = &log, .info = .{ .relays = &.{}, .timeout_ms = 1000 }, .host = "127.0.0.1", .port = 0 }; + + var out = std.Io.Writer.Allocating.init(gpa); + defer out.deinit(); + try handleUnlock(&server, io, &out.writer, "{\"passphrase\":\"correct horse battery staple\"}"); + + const written = out.written(); + try testing.expect(std.mem.indexOf(u8, written, "409") != null); + try testing.expect(std.mem.indexOf(u8, written, "already has this key open") != null); + // And never as a passphrase problem: the passphrase was right. + try testing.expect(std.mem.indexOf(u8, written, "401") == null); +} + test "the key leaving the machine is written down, in the form it left as" { const gpa = testing.allocator; const io = testing.io; diff --git a/daemon/src/onboarding.zig b/daemon/src/onboarding.zig index 595cdb6..56c3341 100644 --- a/daemon/src/onboarding.zig +++ b/daemon/src/onboarding.zig @@ -217,7 +217,7 @@ pub const Gate = struct { /// onboards a fresh key. /// /// This is the destructive half of switching accounts, and the key file is - /// the only copy of that identity on this Mac. Callers must have said so to + /// the only copy of that identity on this machine. Callers must have said so to /// the reader FIRST; nothing here can un-delete it. /// /// The in-memory key is zeroed too, but that is not the whole story and the diff --git a/gui/app.zon b/gui/app.zon index 36c49a2..b343bd8 100644 --- a/gui/app.zon +++ b/gui/app.zon @@ -3,7 +3,7 @@ .name = "notary", .display_name = "Notary", .description = "Notary: approve or deny Nostr signing requests; your key stays in the signer daemon.", - .version = "0.10.9", + .version = "0.10.10", .icons = .{"assets/icon.png"}, .platforms = .{ "macos", "linux" }, .permissions = .{ "view", "command", "clipboard" }, diff --git a/gui/src/app.native b/gui/src/app.native index 47f2360..17c6644 100644 --- a/gui/src/app.native +++ b/gui/src/app.native @@ -48,7 +48,7 @@ - This Mac holds the only copy. Without a backup, losing it loses the identity: a nostr key cannot be replaced. + This computer holds the only copy. Without a backup, losing it loses the identity: a nostr key cannot be replaced. diff --git a/gui/src/main.zig b/gui/src/main.zig index 91150f6..dab6fd7 100644 --- a/gui/src/main.zig +++ b/gui/src/main.zig @@ -342,7 +342,11 @@ pub const Model = struct { relays_len: usize = 0, /// Short human note for the `.daemon_exited` state, e.g. "signer exited /// (code 1)". - exit_note_buf: [64]u8 = [_]u8{0} ** 64, + // 160, not 64. `setExitNote` formats into this and `catch return`s, so a + // message one byte too long is not truncated, it is DROPPED: the reader + // gets a dead window with no note at all, which is the failure the note + // exists to explain. + exit_note_buf: [160]u8 = [_]u8{0} ** 160, exit_note_len: usize = 0, rows: [max_pending]Row = [_]Row{.{}} ** max_pending, @@ -422,7 +426,9 @@ pub const Model = struct { /// A `/setup` or `/unlock` POST is in flight (disables the submit button). submitting: bool = false, - onboard_error_buf: [96]u8 = [_]u8{0} ** 96, + // 160, not 96. The longest message here names two things to quit and was + // silently truncated at 96, which turns an instruction into a fragment. + onboard_error_buf: [160]u8 = [_]u8{0} ** 160, onboard_error_len: usize = 0, // -- config accessors -- @@ -555,7 +561,7 @@ pub const Model = struct { /// /// This window ships inside more than one app. Started from Plaza, the /// signer beside it is Plaza's, and naming Notary's sends a reader to a - /// path that does not exist on their Mac unless they also installed Notary + /// path that does not exist on their machine unless they also installed Notary /// on its own. So the real neighbour wins whenever there is one. /// /// The constant stays for the case it was written for: a dev build with no @@ -884,7 +890,14 @@ pub const Model = struct { fn setExitNote(self: *Model, exit: native_sdk.EffectExit) void { const s = switch (exit.reason) { - .spawn_failed, .rejected => std.fmt.bufPrint(&self.exit_note_buf, "The signer failed to start, check SIGNER_BIN.", .{}), + // Names a path, not an environment variable. `SIGNER_BIN` is a + // developer override documented in gui/README.md, and a reader who + // installed with the one-line installer has never heard of it: the + // message told them to check something they do not have. What is + // actually true for them is that the two binaries have to sit + // together, which is what both packagers arrange and what an + // interrupted install breaks. + .spawn_failed, .rejected => std.fmt.bufPrint(&self.exit_note_buf, "The signer did not start. It has to sit beside Notary in the same folder; installing again puts it back.", .{}), .signaled => std.fmt.bufPrint(&self.exit_note_buf, "The signer was terminated (signal).", .{}), else => std.fmt.bufPrint(&self.exit_note_buf, "The signer exited (code {d}).", .{exit.code}), } catch return; @@ -1789,8 +1802,28 @@ fn onOnboardResponse(model: *Model, fx: *Effects, r: native_sdk.EffectResponse, if (r.outcome == .ok) switch (r.status) { 401 => model.setOnboardError("Wrong passphrase."), 400 => model.setOnboardError(if (kind == .setup) "Check the passphrase and key." else "Bad request."), - // Initialized/unlocked out from under us: re-sync from /info. - 409 => fetchInfo(model, fx), + 409 => { + // TWO different situations answer 409, and they need different + // words. One is benign: the key was initialized or unlocked out + // from under us, so re-syncing from /info lands on the right + // screen. The other is not: another Notary already holds this key + // open (the daemon takes a non-blocking exclusive lock on the key + // file), and re-syncing alone leaves the reader on the unlock + // screen with no error at all. They type the RIGHT passphrase, the + // spinner stops, and nothing happens, forever, with nothing + // anywhere saying why. + // + // Two ways in, both ordinary: installing Plaza and then running + // Notary's installer, which starts a second Notary while Plaza's + // embedded keyholder holds the lock; and re-running the installer + // to upgrade while Notary is open. + // + // The daemon says which it is, so read it rather than guess. + if (std.mem.indexOf(u8, r.body, "already has this key open") != null) { + model.setOnboardError("Another Notary already has this key open. Quit it, or the app that started it, then try again."); + } + fetchInfo(model, fx); + }, else => model.setOnboardError("The signer rejected the request."), } else { model.setOnboardError("Could not reach the signer."); diff --git a/no-such-key.ncryptsec b/no-such-key.ncryptsec deleted file mode 100644 index e69de29..0000000 diff --git a/scripts/install-linux.sh b/scripts/install-linux.sh index 54255dd..e26dbe0 100755 --- a/scripts/install-linux.sh +++ b/scripts/install-linux.sh @@ -34,13 +34,55 @@ set -euo pipefail # `local` is gone by then: under `set -u` the cleanup then dies on its own # variable, which is a confusing failure at the end of a successful install. workdir="" -cleanup() { [ -n "$workdir" ] && rm -rf "$workdir"; } +# `return 0` on purpose. Without it the trap's last command is the failed +# `[ -n "$workdir" ]` of a run that never made a temp directory, and bash exits +# with THAT: `--help` reported failure, and so would any early exit that had not +# reached the download yet. +cleanup() { + [ -n "$workdir" ] && rm -rf "$workdir" + return 0 +} trap cleanup EXIT say() { printf '\033[1m==>\033[0m %s\n' "$1"; } warn() { printf '\033[1;33mnote:\033[0m %s\n' "$1"; } die() { printf '\033[1;31merror:\033[0m %s\n' "$1" >&2; exit 1; } +# Whether GTK 4 is on this machine: `present`, `missing`, or `unknown`. +# +# `ldconfig` is the reliable answer and it lives in /usr/sbin, which Debian does +# NOT put on a normal user's PATH. Gating the whole check on +# `command -v ldconfig` therefore skipped it entirely on one of the three +# distributions this script names as supported: a Debian user without GTK 4 got +# a verified download, a cheerful "Installed Notary", and an app that dies on +# `libgtk-4.so.1` with the launch output thrown away. For an app that holds a +# key, "it will not start" is the worst thing to discover in silence. So it is +# looked for by absolute path too, and if there is no ldconfig at all the +# library directories are searched directly. +# +# `grep -c ... || true`, NOT `grep -q`. Under `set -o pipefail` a matching +# `grep -q` exits at once, `ldconfig` dies of SIGPIPE, and the pipeline reports +# THAT rather than the match, so the guard fires on a machine that HAS GTK. It +# fires on one that does not either, because grep exits 1 there, which makes it +# a check that can never pass. `grep -c` drains its input instead. +gtkStatus() { + local ldc hits d + for ldc in ldconfig /usr/sbin/ldconfig /sbin/ldconfig; do + command -v "$ldc" >/dev/null 2>&1 || [ -x "$ldc" ] || continue + hits="$("$ldc" -p 2>/dev/null | grep -c 'libgtk-4\.so' || true)" + if [ "$hits" = "0" ]; then printf 'missing\n'; else printf 'present\n'; fi + return + done + for d in /usr/lib /usr/lib64 /lib /lib64 /usr/local/lib /usr/lib/*-linux-gnu*; do + [ -d "$d" ] || continue + if compgen -G "$d/libgtk-4.so*" >/dev/null 2>&1; then printf 'present\n'; return; fi + done + # No ldconfig and nothing in the usual places. Refusing here would turn an + # unusual layout into a refused install, so this reports that it cannot tell + # and the caller warns rather than dies. + printf 'unknown\n' +} + # All work happens inside main(), invoked on the very last line, so bash runs # nothing until the whole script has been read. A truncated `curl | bash` (a # connection dropped mid-stream) then does nothing at all rather than half of @@ -54,7 +96,12 @@ main() { while [ $# -gt 0 ]; do case "$1" in --archive) archive="${2:?--archive needs a path}"; shift 2 ;; - *) die "unknown argument: $1" ;; + -h | --help) + printf 'usage: install-linux.sh [--archive ]\n\n' + printf ' --archive install this tarball instead of the latest release\n' + exit 0 + ;; + *) die "unknown argument: $1 (try --help)" ;; esac done @@ -65,33 +112,18 @@ main() { command -v "$tool" >/dev/null 2>&1 || die "$tool is required and is not on PATH." done - # GTK 4 is the one runtime dependency, and finding out it is missing when the - # window fails to open is worse than being told now. Checked by loader rather - # than by package name, because the package is called libgtk-4-1 on Debian and - # Ubuntu, gtk4 on Fedora and Arch, and something else again elsewhere. - # - # `grep -c ... || true`, NOT `grep -q`. Under `set -o pipefail` a matching - # `grep -q` exits at once, `ldconfig` dies of SIGPIPE, and the pipeline reports - # THAT rather than the match, so the guard fires on a machine that has GTK. It - # fires on one that does not either, because grep exits 1 there, which makes it - # a check that can never pass. `package-linux.sh` carries a comment about this - # exact trap and I wrote it here anyway. - if command -v ldconfig >/dev/null 2>&1; then - local gtk - gtk="$(ldconfig -p 2>/dev/null | grep -c "libgtk-4\.so" || true)" - [ "$gtk" != "0" ] || die "GTK 4 is missing. Install it first: apt install libgtk-4-1, dnf install gtk4, or pacman -S gtk4." - fi - # The distribution floor, checked BEFORE downloading anything or writing files. # Without this an Ubuntu 22.04 user gets a clean install, a verified digest, a # cheerful "Installed Notary", and then `version GLIBC_2.38 not found` the # first time they open it. For an app that holds a key, "it will not start" # should never be something you discover after trusting it with one. # - # glibc is the proxy for both floors. The real constraints are glibc 2.38 (the - # binaries are built on Ubuntu 24.04) and GTK 4.10 (the toolkit's own declared - # floor), and every distribution that has one has the other, so one check - # answers both and needs no -dev package to run. + # glibc is the proxy for the DISTRIBUTION GENERATION, not a statement about + # these binaries. Notary's own two binaries need only glibc 2.36; what needs a + # newer system is GTK 4.10, the toolkit's declared floor, and there is no way + # to read GTK's minor version without a -dev package. Every distribution + # carrying glibc 2.38 carries GTK 4.10, so this reads the one that is always + # legible and gates on it. local glibc glibc="$(ldd --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+$' || true)" if [ -n "$glibc" ]; then @@ -99,13 +131,24 @@ main() { major="${glibc%%.*}" minor="${glibc##*.}" if [ "$major" -lt 2 ] || { [ "$major" -eq 2 ] && [ "$minor" -lt 38 ]; }; then - die "this build needs glibc 2.38 or newer and GTK 4.10 or newer; you have glibc $glibc. + die "this build needs GTK 4.10 or newer, which means a system newer than yours (glibc $glibc). That means Ubuntu 23.10+, Debian 13+, or Fedora 39+. Ubuntu 22.04 and Debian 12 are too old for it. Building from source on your own system works if its GTK is 4.10 or newer: https://github.com/zig-nostr/notary#build" fi fi + # GTK 4 itself, AFTER the floor above. The order matters: an Ubuntu 22.04 user + # has GTK 4.6, so a GTK-presence check passes and then tells them nothing, + # while the floor tells them the true reason their machine cannot run this. + # Checked by loader rather than by package name, because the package is called + # libgtk-4-1 on Debian and Ubuntu, gtk4 on Fedora and Arch, and something else + # again elsewhere. + case "$(gtkStatus)" in + missing) die "GTK 4 is missing. Install it first: apt install libgtk-4-1, dnf install gtk4, or pacman -S gtk4." ;; + unknown) warn "could not tell whether GTK 4 is installed on this system. If Notary does not open, that is the first thing to check." ;; + esac + local arch arch="$(uname -m)" case "$arch" in @@ -157,15 +200,24 @@ main() { # The digest is published beside the tarball rather than read out of the API # body, so a release whose notes were edited cannot change what this compares # against. - if curl -fsSL -o "$tmp/$asset.sha256" "$url.sha256" 2>/dev/null; then - local want got - want="$(awk '{print $1}' "$tmp/$asset.sha256")" - got="$(sha256sum "$tmp/$asset" | awk '{print $1}')" - [ "$want" = "$got" ] || die "the download does not match its published SHA-256. Not installing it." - say "SHA-256 verified." - else - warn "no published SHA-256 for this release, so the download could not be verified." - fi + # Required, not best-effort. It used to warn and install anyway when the + # sidecar could not be fetched, which is a verification step that any + # transient failure switches off. On an app that holds a key that is not a + # trade-off worth making: every published release has a digest, so a missing + # one is a reason to stop. + curl -fsSL --retry 2 --retry-all-errors -o "$tmp/$asset.sha256" "$url.sha256" 2>/dev/null || + die "could not fetch the published SHA-256 for $asset, so the download cannot be verified. Not installing it. + Try again, or download the tarball and its .sha256 by hand and pass --archive." + local want got + want="$(awk '{print $1}' "$tmp/$asset.sha256")" + # An empty expected digest compares equal to an empty computed one, and the + # whole check then reports success over nothing at all. Both sides are + # required to exist before either is trusted. + [ -n "$want" ] || die "the published SHA-256 for $asset is empty. Not installing it." + got="$(sha256sum "$tmp/$asset" | awk '{print $1}')" + [ -n "$got" ] || die "could not compute the SHA-256 of the download. Not installing it." + [ "$want" = "$got" ] || die "the download does not match its published SHA-256. Not installing it." + say "SHA-256 verified." installFrom "$tmp" "$asset" "$tag" } @@ -175,7 +227,8 @@ main() { installFrom() { local tmp="$1" asset="$2" tag="$3" say "Unpacking..." - tar -C "$tmp" -xzf "$tmp/$asset" + tar -C "$tmp" -xzf "$tmp/$asset" 2>/dev/null || + die "the archive could not be unpacked. The download may be incomplete, or the file passed to --archive may not be a Notary tarball." local src src="$(find "$tmp" -maxdepth 1 -type d -name 'notary-*-linux-*' | head -1)" [ -n "$src" ] || die "the archive did not contain what was expected." @@ -229,10 +282,28 @@ installFrom() { *) warn "$prefix/bin is not on your PATH. Add it to run 'notary' from a terminal; the desktop entry works either way." ;; esac - say "Installed Notary $tag." + if [ "$tag" = "local" ]; then + say "Installed Notary from $asset." + else + say "Installed Notary $tag." + fi + + # Started with its output kept, briefly. It used to go to /dev/null, so a + # first run that died on a missing library was indistinguishable from a + # working install: the script said "Starting it...", nothing appeared, and + # nothing anywhere said why. On the app that holds your key, that silence is + # the worst possible failure mode. If it is still alive a moment later the log + # is dropped and it is left to run. say "Starting it..." - "$prefix/bin/notary" >/dev/null 2>&1 & + local log pid + log="$tmp/first-run.log" + "$prefix/bin/notary" >"$log" 2>&1 & + pid=$! disown 2>/dev/null || true + sleep 2 + kill -0 "$pid" 2>/dev/null && return + warn "Notary exited immediately. It is installed at $prefix/bin/notary. This is what it said:" + sed 's/^/ /' "$log" >&2 || true } main "$@"