Skip to content

feat: deed 0.4.0, Windows builds, store directories and errors, NIP-19 in capitals - #32

Merged
sepehr-safari merged 8 commits into
mainfrom
deed-on-windows
Oct 4, 2026
Merged

sepehr-safari merged 8 commits into
mainfrom
deed-on-windows

Conversation

@sepehr-safari

Copy link
Copy Markdown
Contributor

deed 0.4.0: Windows builds, a store that makes its own directories and explains a failed open, and NIP-19 codes in capitals.

  • Windows. x86_64-windows-gnu and aarch64-windows-gnu build, and the release workflow packs each into a zip holding deed.exe, LICENSE and README, with a .sha256 beside it. The two store-permission tests skip there, since Windows has no mode bits.
  • The store. --store creates missing directories, ~ comes from HOME, and a store that will not open says why (no permission, not a store, a directory in the way). --local only reads, so a mistyped path no longer leaves an empty store behind.
  • NIP-19 codes. An all-uppercase code decodes, as BIP-173 allows, and so does NOSTR: in any case. Mixed case is still refused.
  • ARCHITECTURE.md. The README, AGENTS.md, the skill and the help text are updated to match.
  • Moves to nostr v0.14.8. That release builds for Windows, and its read deadline, which req and fetch --timeout use, works there.

Tested: zig build test (116/116) and zig fmt --check . on macOS, plus Windows builds and test-binary cross-compiles for both architectures.

deed now builds for x86_64-windows-gnu and aarch64-windows-gnu. The library underneath builds there now, which left three things in deed itself. Arguments: the iterator that splits the command line on Windows needs an allocator, so main uses initAllocator, which does nothing off Windows. Output: a Windows console decodes bytes with its own code page, so the UTF-8 in the help text and the messages came out as mojibake. A small console module switches the code pages to UTF-8 for the length of the run and does nothing elsewhere. Tests: the test relay shrinks a socket's receive window, and posix setsockopt does not exist on Windows.

The code page belongs to the console, so every process attached to it shares it. The output page is switched only when stdout is the console, and the input page only when stdin is, so in `deed key generate | deed event` only the second deed switches the output page and the first one exiting cannot put it back under the second one's output. The pages are restored before exit, on a failed setup in main, and from a console control handler on Ctrl-C, which is how a `req --stream` run ends. The choice of which page to switch is a pure function with a test that runs on every host.

std's Windows sockets are AFD handles opened without Winsock, so Winsock's setsockopt does not know them. The test relay sends SO_RCVBUF as the same AFD request std uses for its own socket options, and treats it as best effort, since the two modes that use it fill a larger window too, only later.

Environment variables, standard input and output, and the exit codes needed no change. There is no default store directory, so no path to fix: --store takes whatever path it is given. LMDB sizes the store file to its whole map (1 GiB) up front on Windows, and the README says so.

Plain `zig build -Dtarget=x86_64-windows-gnu` passed before the test relay was fixed, because the executable never reaches it. Building the test binary is what catches that, so AGENTS.md says to use `zig build test -Dtarget=x86_64-windows-gnu` as the Windows check, whose run step then fails because the host cannot run the result.

The release workflow builds both Windows targets and publishes each as a zip holding deed.exe, LICENSE and README.md, with a .sha256 beside it, named like the other targets (deed-<version>-windows-x86_64.zip). The downloaded zips are unpacked and run on Windows runners before the draft is lifted, and the page check now expects twelve assets. CI gets a windows-latest job that builds, runs the tests and runs the binary it built. The README has a Windows install section whose PowerShell lines check the SHA-256, take either architecture's zip, and add the folder to PATH only once, so they also upgrade. The installer script points a Git Bash or MSYS shell at the zip instead of saying there is no Windows build, the release notes name Windows, and the skill no longer says there is no Windows build.
ARCHITECTURE.md describes how deed is put together: what it is, where each command lives, how a run flows from arguments through the filter or event to dialling relays and the store, how deadlines and cancellation work, the store, the exit codes, the platforms (including what deed itself does for Windows and the size of a store there), and how it is tested, including the test relay and the benchmarks. It also records that publish keeps one reader per relay while req and fetch poll the relays in turn from one thread, so the next change in that area starts from what the code does. README and AGENTS.md link to it.
`--store ~/.deed/db` failed on a machine with no ~/.deed yet, and every failure to open a store printed `Lmdb`, because LMDB reports every reason as one status. `req` and `fetch` now share one way of opening the store named by `--store`.

When a run will write, the directories above the store are created first, as `mkdir -p` would, relative paths and `..` included. When that fails, the message names the directory and the cause: permission denied, a file in the way, a read-only file system, a full disk.

When the open itself fails, the path is examined to say why: it is a directory, it cannot be read and written, its lock file cannot be made, or none of those, in which case it is probably not a store. A file that is not a store is left untouched.

`req --local` only reads, so it opens only a store that already exists and says "there is no store at" a path that has none. Before, it quietly created an empty store there, and printed nothing.

A leading `~` is the home directory. The shell expands it only when unquoted, and a quoted one would otherwise have created a directory literally named `~` in the working directory. `~user` is refused rather than guessed at.
Bech32 allows a code to be all lowercase or all uppercase, and a QR code carries the uppercase form, but deed sniffed the entity with a case-sensitive prefix match before handing the code to the decoder, so `deed decode NPUB1...` came back as "not a code deed knows". The prefix checks in decode, fetch, req and the key readers now ignore ASCII case, and the `nostr:` scheme is stripped in any case (the library's own stripper only matches lowercase). A mixed-case code still reaches the decoder and is refused there as mixed case.

The matching lives in two small helpers in keyinput.zig, which already holds the forms a key or code can arrive in. Tests cover each entry point: decode for every entity type with and without an uppercase NOSTR: prefix, fetch resolution for note, nevent, naddr, nprofile and npub, req author and id arguments, and the nsec and npub key readers used by crypt, event and key.

Part of zig-nostr/nostr#105
…with the store and code-case changes

A new store's directories are created and a store that will not open says why, NIP-19 codes are accepted in upper case, and the docs and help text now say so, with ARCHITECTURE.md matching storepath.zig.
The two tests that take write permission away from a directory or a store file set POSIX mode bits, which Windows does not have, so the Windows test build did not compile. They now skip there, the same way they skip when running as root.
Windows zips, a store that creates its directories and says why it will not open, and NIP-19 codes in capitals. Moves to nostr v0.14.8, which carries the Windows support and the read deadline that works there.
…as a close does

When a relay drops an idle connection, POSIX takes the next send and reports the close on the read after it, and that close already earns one fresh connection and one more try. Windows refuses the send itself, and a refused send was reported as a failure with no retry, so the event did not go out. A send that fails outright now gets the same one retry inside the same deadline. The dial test also accepts a timeout for the closed port on Windows, which retries a refused connect for about two seconds.
@sepehr-safari
sepehr-safari merged commit 130adde into main Oct 4, 2026
3 checks passed
@sepehr-safari
sepehr-safari deleted the deed-on-windows branch October 4, 2026 16:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant