CLAUDE.md (19325B)
1 # CLAUDE.md 2 3 This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 5 ## What this is 6 7 A Windows **installer** for the GNU Taler merchant backend. There is no Taler source code 8 here — the backend is installed from the `deb.taler.net` APT repository *inside WSL*. This 9 repo is only the orchestration layer: an NSIS wizard that collects user choices, a batch 10 script that drives `wsl.exe`, and three bash scripts that run inside the WSL distro. 11 12 Twelve tracked files, no build system, no test suite. It is also the odd one out in the 13 `/home/claude` workspace: nothing here links against `~/target`, and the build-order and 14 `make install` rules in the parent `CLAUDE.md` do not apply. 15 16 ## Building 17 18 ```sh 19 makensis taler-installer-taler.nsi # produces taler-installer.exe (gitignored) 20 ``` 21 22 `makensis` is not installed in this workspace, but it does **not** have to be: the Debian 23 package can be unpacked into the scratchpad without root and run from there, which does 24 compile this installer (verified with 3.11). That catches every syntax error, missing 25 `LangString` and encoding problem — do it instead of reviewing the `.nsi` by eye: 26 27 ```sh 28 cd "$SCRATCH" && apt-get download nsis nsis-common 29 for f in *.deb; do dpkg-deb -x "$f" root; done 30 cd /home/claude/taler-windows 31 NSISDIR="$SCRATCH/root/usr/share/nsis" "$SCRATCH/root/usr/bin/makensis" -WX \ 32 taler-installer-taler.nsi && rm -f taler-installer.exe 33 ``` 34 35 Only *running* the result needs Windows with WSL. Required at compile time in the same 36 directory: `logo-header.ico`, `logo-header.bmp`, `logo-welcome.bmp`, and the four scripts 37 listed in the `File` directives. 38 39 ## Control flow 40 41 ``` 42 taler-installer.exe (NSIS, RequestExecutionLevel admin) 43 wizard pages: Welcome → Directory → Distro → Domain → ReverseProxy → INSTFILES 44 Section "Install Taler-Merchant" installs the 4 scripts to $INSTDIR, then: 45 setup-wsl-debian.bat 1 "" <distro> → steps 1 only 46 setup-wsl-debian.bat ALL "" <distro> → steps 1..8 47 setup-wsl-debian.bat 8 <domain> <distro> <proxy_mode> → step 8 only 48 post-install pages: AskExternalDB → DBUrl → calls test-db.sh / set-db.sh in WSL 49 .onGUIEnd: restarts apache2, opens http(s)://<domain> in the browser 50 ``` 51 52 `setup-wsl-debian.bat` is a **step dispatcher**: `%1` is a step number (`1`-`8`) or `ALL`. 53 It `goto`s the matching `:stepN` label and each step ends with 54 `if not "%STEP%"=="ALL" exit /b`, so a numeric argument runs one step and `ALL` falls 55 through all of them. Arguments are positional: `STEP DOMAIN DISTRO PROXY_MODE`. 56 57 The steps: (1) enable `Microsoft-Windows-Subsystem-Linux` + `VirtualMachinePlatform`, 58 `wsl --update`; (2) `wsl --install -d <distro>`; (3) create the `taleruser` account, 59 grant it `NOPASSWD` sudo, make it the default in `/etc/wsl.conf`, terminate the distro to 60 apply; (4) add it to `www-data`; (5) copy the three `.sh` files through `%TEMP%` into 61 `/home/taleruser/taler-installer` via `type | wsl tee`, `dos2unix`, `chmod +x`; 62 (6) verify format/permissions; (7) `wsl --shutdown`, then run 63 `install-taler-from-apt.sh`; (8) install Apache and run `taler-merchant-rproxy-setup`. 64 65 `install-taler-from-apt.sh` is likewise **multi-mode on `$1`**: `8 <domain>` does the 66 HTTPS reverse proxy only, `http <domain>` the HTTP-only proxy only, and no recognised first 67 argument means the full install (postgres, Taler APT repo keyring for the detected 68 Debian/Ubuntu codename, `taler-merchant`, certbot, `taler-merchant-dbconfig`, enable the 69 systemd units). Both proxy branches are currently dead from the batch path, because step 8 70 calls `taler-merchant-rproxy-setup` itself. 71 72 Fixed identities the scripts assume everywhere: WSL user `taleruser`, Taler DB role 73 `taler-merchant-httpd`, scripts at `/home/taleruser/taler-installer/`. 74 75 ## The contract with the taler-merchant package 76 77 Most of this installer's history of breakage came from fighting the Debian package 78 instead of using it, so know what `taler-merchant` already provides: 79 80 - **`/etc/taler-merchant/taler-merchant.conf` is an entry point, not a settings file.** 81 It carries `[paths]` and three include directives: `@inline-matching@ conf.d/*.conf` 82 (which is where `[merchant] SERVE = systemd` lives), `@inline@ overrides.conf`, and 83 `@inline-secret@ merchantdb-postgres ../secrets/merchant-db.secret.conf`. 84 **Put overrides in a `conf.d/` drop-in**, never in this file. 85 Overwriting it is *not* immediately fatal — measured, not assumed: the defaults under 86 `/usr/share/taler-merchant/config.d/` still supply `SERVE = tcp` and 87 `CONFIG = postgres:///taler-merchant`, and systemd socket activation overrides 88 `SERVE = tcp` anyway, so the backend still answers. What is lost is everything an 89 administrator puts under `/etc/taler-merchant`: every `conf.d/` drop-in, `overrides.conf`, 90 and the database secret are silently no longer included. 91 - **`SERVE` must be `tcp`, `unix` or `systemd`** (`exchange/src/mhd/mhd_config.c:194`); 92 if it resolves nowhere at all, `taler-merchant-httpd` aborts with *"serve type (tcp, 93 unix or systemd) required"*. When `LISTEN_FDS` is set, systemd activation wins over 94 `SERVE` and only logs a warning. There is no `UNIX_DOMAIN_SOCKETS` option anywhere in 95 the Taler codebase. 96 - **The backend is socket-activated.** `taler-merchant-httpd.socket` listens on 97 `/run/taler-merchant/httpd/merchant-http.sock` (`taler-merchant-httpd:www-data`, mode 98 0660) and the service has `Requires=` it. Enable the *socket* (or the target), not the 99 service. There is no need for a TCP port such as 8888. 100 - **The package ships the Apache vhost** at `/etc/apache2/sites-available/taler-merchant.conf`, 101 already proxying to that unix socket, with `%%your.domain%%` placeholders that 102 `taler-merchant-rproxy-setup` substitutes. 103 - **The database URI** goes in `secrets/merchant-db.secret.conf` as 104 `[merchantdb-postgres] CONFIG=postgres://...`. 105 106 `taler-merchant-rproxy-setup` (in the `merchant` checkout) accepts **only** `--domain`, 107 `--nginx|--apache`, `--httponly|--httpsonly` and `-h`. Any other argument is a hard 108 `exit 1`. It runs `certbot certonly --webroot -w /var/www/html`, so plain `certbot` is 109 the only certbot package needed — **never** `python3-certbot-nginx`, which `Depends: nginx` 110 and leaves nginx holding port 80 so the apache2 installed in step 8 cannot start. 111 112 ## Logs 113 114 Scattered, and worth knowing before debugging a user report: 115 116 | Path | Written by | 117 | --- | --- | 118 | `C:\Users\Public\install.log` | the `.nsi` itself, via `TalerLog` (truncated at each start) | 119 | `C:\Users\Public\taler-bat.log` | `setup-wsl-debian.bat` (truncated at each start) | 120 | `C:\Users\Public\taler-sh.log` | `install-taler-from-apt.sh` (via `exec > >(tee -a)`) | 121 | `C:\Users\Public\install-bat.log` | copied out of WSL at the end of step 7 | 122 | `/home/taleruser/taler-installer/install-sh.log`, `test-db.log` | the bash scripts | 123 | `/tmp/dbtest.log` | `test-db.sh` | 124 125 `install.log` is the Windows-side timeline that `readme.txt`, `MSG_INSTALL_FAILED` and 126 `.onInstFailed` have always promised: the machine and Windows build, the four wizard 127 answers, the exit code of every `nsExec` call, the reverse-proxy probe, the verdict, and a 128 closing list of the other log paths. It exists because when WSL is what failed, every log 129 in the table below it is unreachable. Rules for extending it: 130 131 - **Write through `${TLog}` (log only) or `${TSay}` (log *and* details view),** never a 132 bare `DetailPrint` for anything diagnostic. `TalerLog` reopens the file per line on 133 purpose — the installer can be killed inside a ten-minute `nsExec` call, and what was 134 written before that has to already be on disk. 135 - **Nothing secret may be written there.** `C:\Users\Public` is readable by every account 136 on the machine. The external-database URL carries a password, so it goes through 137 `RedactDbUrl` (drop everything through the last `@`) before it reaches either the log or 138 the details view; `test-db.sh` does the same with `${1##*@}` for its own two logs. 139 - **Do not log the `[2/8]`..`[6/8]` lines.** They are cosmetic `DetailPrint`s; all of that 140 work actually happens in the single `setup-wsl-debian.bat ALL` call, and a log claiming 141 six steps that never individually ran would be worse than no log. 142 - `nsExec::ExecToLog` pushes one value, `ExecToStack` pushes the output *and then* the exit 143 code, so the latter needs two `Pop`s (verified in `Contrib/nsExec/nsexec.c`; it pushes a 144 single value only when it cannot build a command line at all, which needs an empty 145 command string). Several calls here used to pop nothing and both lost the diagnostic and 146 left entries on the global stack. 147 148 ## Test harness 149 150 There is a working harness on `tripwire.taler.net` under `/srv/winstall-test` 151 (not in this repo yet). It is the only way these scripts get exercised, since nothing 152 here can run on the development workstation. 153 154 ``` 155 harness/run-debian-test.sh <fresh|twice|reinstall> <ssh-port> <repo-dir> <domain> [label] 156 ``` 157 158 It boots a throwaway Debian 13 cloud image under qemu (user-mode networking, qcow2 159 overlay destroyed afterwards), creates `taleruser` the way the batch file would, runs 160 `install-taler-from-apt.sh` plus `taler-merchant-rproxy-setup`, and asserts the result — 161 ending with a real `GET /config` both over the backend's unix socket and through Apache. 162 `twice` runs the installer a second time on a dirty system; `reinstall` purges 163 `taler-merchant` in between. Point `<repo-dir>` at a checkout of `master` or of a fix 164 branch to get a before/after. 165 166 Two traps cost time when extending it: `ssh` inside the driver needs `-n` or it eats the 167 script's stdin and hangs forever, and assertion patterns must use `grep -F` — `[paths]` 168 as a regex is a character class that matches almost any line. 169 170 ## Verified end to end on Windows 171 172 Three scenarios, each from a fresh overlay of a clean Windows 11 Enterprise 173 Evaluation (build 26300) with the WSL features *disabled* and no WSL runtime -- 174 i.e. a real first install. All three pass 14/14, ending with 175 `http://localhost/config` answered **from Windows**, which is what a merchant's 176 browser hits: 177 178 | scenario | what it does | result | 179 | --- | --- | --- | 180 | `fresh` | enable features, reboot, MSI bootstrap, distro + user, Taler, HTTP proxy | 14/0 | 181 | `twice` | the above, then the whole installer again over existing state | 14/0 | 182 | `reinstall` | the above, then `wsl --unregister` and install again | 14/0 | 183 184 Run them with `harness/win-test.sh run <fresh|twice|reinstall>`; they use 185 separate ports, overlays and firmware vars, so all three run in parallel. 186 187 Two traps when editing the batch file, both of which silently aborted a run: 188 `cmd` parses `%~` substitutions **even inside `::` comments**, and `start`, 189 `timeout` and `pause` all refuse to run with redirected stdin. 190 191 ## What the Windows VM proved 192 193 Measured on a clean Windows 11 Enterprise Evaluation (build 26300, 26H2) under 194 qemu/KVM, not inferred: 195 196 - **Step 1 is fine.** Both optional features go `Disabled` -> `Enabled`, the 197 reboot gate works, and the machine comes back. 198 - **WSL2 itself runs** in that nested setup: `vmmemWSL` live, `wsl -l -v` 199 reporting `Debian / Running / VERSION 2`. WSL prints *"nested virtualization is 200 not supported on this machine"*, which concerns nesting *inside* WSL and is not 201 fatal. 202 - **`cmd`'s `start` and `timeout` refuse redirected stdin**, printing 203 *"Input redirection is not supported, exiting the process immediately"*. nsExec 204 runs this batch file exactly that way, so `wsl --update` never executed and all 205 five `timeout` delays were silent no-ops. Use `ping -n N 127.0.0.1 >nul` for a 206 delay in this file, never `timeout`. 207 - **`wsl --install -d <distro>` blocks on an invisible prompt** -- 208 *"Please create a default UNIX user account"* -- observed hung for 54 minutes. 209 `--no-launch` is required; step 3 creates `taleruser` itself anyway. 210 - **The in-box `wsl.exe` is only a stub.** Before the WSL runtime is present it 211 answers *"The Windows Subsystem for Linux is not installed"* to every form 212 except a bare `--install`, prints no `--help` at all, and cannot reliably 213 bootstrap itself on an image without the Store. Installing the official MSI 214 (`wsl.<ver>.x64.msi` from `microsoft/WSL` releases) with 215 `msiexec /i wsl.msi /quiet /norestart` works silently and returns 0 — that is 216 the deterministic way to get WSL onto a merchant machine. 217 - **`wsl --install` cannot create the UNIX account.** Its complete option set is 218 `--enable-wsl1 --fixed-vhd --from-file --legacy --location --name 219 --no-distribution --no-launch --version --vhd-size --web-download`. The 220 account is only ever created by the distro's interactive first-run. The 221 supported non-interactive sequence, verified end to end in the VM, is: 222 `wsl --install -d <Distro> --no-launch --web-download`, then 223 `wsl -d <Distro> -u root -- useradd -m <user>`, then 224 `wsl --manage <Distro> --set-default-user <user>`. 225 - **systemd is already PID 1 in the Debian WSL image.** It ships `/etc/wsl.conf` 226 containing `[boot] systemd=true`, `ps -p 1 -o comm=` returns `systemd` and 227 `systemctl is-system-running` returns `running`. The `systemctl` calls these 228 scripts make are therefore fine — but hand-appending `[user]` to that file 229 risks clobbering the `[boot]` stanza, which is why `--set-default-user` is 230 preferred. 231 - **The installer always claims success.** The final 232 *"GNU Taler-Merchant installed successfully"* line prints unconditionally, two 233 seconds after the log records `return code 1` from the reverse-proxy step. 234 235 ## Known rough edges 236 237 Fixed on the current review branch (see `git log`): the non-existent 238 `--acme`/`--merchant-url` arguments, the `%errorlevel%` expansion bug that hid every 239 reverse-proxy failure, the `taler-merchant.conf` overwrite, the `python3-certbot-nginx` 240 port-80 conflict, enabling the service instead of the socket, `set-db.sh` writing a 241 `DATABASE_URL` key that nothing reads, the mid-step `exit /b` that made `STEP=3` do only 242 half of step 3, and `$DOMAIN_INPUT` being used both as an nsDialogs control handle and as 243 the domain text (which broke the "localhost implies HTTP-only" guard). 244 245 Still open, and worth knowing before debugging a user report: 246 247 - **`rproxy-setup` requires the domain to resolve to a local IP.** It runs 248 `dig +short A "$DOMAIN"` and compares against `hostname -I`, exiting 1 on a mismatch. 249 Inside WSL the interface is NAT'd (172.x), so a real public domain never matches, and 250 `localhost` has no A record at all. The check is skipped entirely when `dig` is absent 251 — `bind9-dnsutils` is priority-standard but not a dependency of `taler-merchant`, 252 `certbot`, `apache2` or `postgresql`, so whether this fires depends on the WSL image. 253 Fixing it properly means relaxing the check to a warning in the `merchant` checkout 254 (its own comment suggests exactly that), which is a separate repo and review. 255 - **`certbot certonly` is interactive** (email, TOS) and `rproxy-setup` passes no 256 `--non-interactive --agree-tos -m`, so HTTPS mode can block on a prompt that nobody 257 sees behind `nsExec`. Also a `merchant`-side change. 258 - **No silent/unattended mode.** All four inputs come from `nsDialogs` pages, which `/S` 259 skips, leaving the variables empty; the ~8 `MessageBox` calls block regardless of `/S` 260 (NSIS does not suppress them). `FileFunc.nsh` is already included, so `GetOptions` for 261 `/DOMAIN=` etc. is close at hand. 262 - **Step 1 demands a reboot and a full re-run**, persisting no state, so the wizard 263 re-asks everything. For automated runs, snapshot a VM with the WSL features already 264 enabled. 265 - **`.onGUIEnd` shows a MessageBox and opens a browser on any exit**, including user 266 cancel and failure. (It now at least logs which of those happened.) 267 - **Progress percentages are fake** — `Sleep`-driven `DetailPrint` lines, because the 268 long `nsExec` calls block the GUI. 269 - **Start-menu shortcut** points at `$INSTDIR\taler-installer.exe`, which the section 270 never installs. 271 - **`%DATE%`/`%TIME%` inside parenthesised blocks** all expand at parse time, so every 272 log line in one block shares a timestamp. 273 - Timestamps aside, `MessageBox`/`DetailPrint` text and the log messages are a mix of 274 English and French. 275 276 ## Conventions 277 278 - Shell scripts are committed **LF**; `.bat` and `.nsi` are **CRLF** (NSIS and `cmd.exe` 279 want it). Step 5 runs `dos2unix` in WSL as a belt-and-braces measure, so a stray CRLF in 280 a `.sh` survives — keep them LF anyway. 281 - GPLv2.1+ header with `Author: Léa Oualli` on the `.bat`/`.nsi`/`.sh` files that have one. 282 - Comments and some log messages are in **French**; user-facing strings are localised into 283 six languages. Every new wizard string needs a `LangString` for all of 284 `${LANG_ENGLISH}`, `${LANG_FRENCH}`, `${LANG_GERMAN}`, `${LANG_ITALIAN}`, 285 `${LANG_SPANISH}` and `${LANG_DUTCH}` — an id missing from one language falls back to 286 the first declared language (English) with no warning, so keep the six lines per id 287 adjacent and check with 288 `grep -c 'LANG_GERMAN}' taler-installer-taler.nsi` against the other five. 289 - **`taler-installer-taler.nsi` must keep its UTF-8 BOM.** `Unicode true` governs the 290 *output*, not how the *source* is read. The BOM is what makes the source decode as UTF-8 291 unconditionally: measured with `makensis` 3.11, a BOM'd script compiles as UTF-8 even 292 when overridden with `-INPUTCHARSET CP1252` (all accents intact), while the same script 293 with the BOM stripped obeys that codepage and turns 3 of 4 probe strings into mojibake. 294 BOM-less UTF-8 is merely auto-detected as a fallback, which is why it happens to work on 295 a Linux host whose ACP is already UTF-8 and cannot be relied on for a Windows build. 296 Dropping the BOM therefore corrupts every `ä ö ü ß è à á í ñ ¿ ¡ ï` in a `LangString` — 297 that is why the French strings used to be written unaccented ("Etape", "redemarrer"). 298 They may now be accented properly; several still are not. 299 - Never put U+202F (narrow no-break space, the French space before `?`/`!`) in a 300 `LangString`. It exists in no Windows ANSI codepage and renders as three stray glyphs 301 if the BOM is ever lost. 302 - Translated terminology comes from the workspace glossary `../dictionary.csv`, not from 303 taste: `merchant` is Händler / comerciante / commerçant / venditore / verkoper, and 304 `backend` stays an English loanword inflected natively. 305 - `.onInit` exists only to `!insertmacro MUI_LANGDLL_DISPLAY`. The `MUI_LANGDLL_*` defines 306 near the top configure that dialog but do not show it; drop the `.onInit` and the 307 installer silently runs in the system language, or English, and every translation 308 becomes unreachable. 309 - `test-if.bat` is a scratch file for checking the `MODE`/`TYPE` branch logic, not part of 310 the installer. `run-admin.bat` just re-launches `setup-wsl-debian.bat` elevated via 311 PowerShell for manual testing outside the wizard. 312 313 ## Git 314 315 Same workspace rule as the sibling checkouts: `origin` is a read-only mirror; work goes on 316 `dev/phoenix-bot/<topic>` pushed to the `durian` remote, via the **`taler-push`** skill. 317 This repo's history, unlike the other checkouts', is a handful of informal French commit 318 messages — follow the skill's format for new commits rather than that precedent.