taler-windows

Windows packaging
Log | Files | Refs

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.