diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-25 17:32:53 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-25 17:32:53 +0200 |
| commit | 1cc12b86dfb036ea4ee5100ca6653d3c3b054195 (patch) | |
| tree | ce3dfa3e9b288cd1090f5c6839a481fa200d7ff2 | |
| parent | 6ea6980d064d1a27470b077c17adab286e4510b1 (diff) | |
| download | qtmaildir-1cc12b86dfb036ea4ee5100ca6653d3c3b054195.tar.gz qtmaildir-1cc12b86dfb036ea4ee5100ca6653d3c3b054195.zip | |
feat: number each build of a dev tree
The version alone cannot tell one build of an unreleased X.Y.Z from
another, and the user rebuilds and hand-tests unreleased builds daily.
They chose a counter over a git description: what they want to know is
that the binary is newer than the one they were running, not which
commit it came from.
QTMAILDIR_BUILD_NUMBER is a cmake option, ON by default, that runs
cmake/BuildNumber.cmake as a build step to increment a counter and write
buildnumber.h. It had to be a build step: configure_file runs once per
cmake run, so a counter interpolated into version.h.in would sit still
across exactly the rebuilds this exists to distinguish, which is why
version.h.in includes a second generated header rather than carrying the
number itself.
Two macros, and the split is load-bearing. QTMAILDIR_VERSION stays a
clean X.Y.Z and keeps the window title, applicationVersion and anything
that might ever compare versions; QTMAILDIR_VERSION_DISPLAY carries the
number and goes to the three surfaces the user picked, --version and
--help, the About dialog, and the placeholder pane. The window title was
offered and declined, since the number would then be in every
screenshot.
The counter lives in the build directory and is not tracked, so it
cannot conflict on a pull or leave the tree dirty; a fresh build
directory restarts at 1, which is honest, because it is a different
build tree. A release passes -DQTMAILDIR_BUILD_NUMBER=OFF and the header
is written empty. The SlackBuild in the my-slackbuilds repo needs that
flag and is a separate commit there.
Verified by running it, since none of this is reachable from a C++ test:
three consecutive builds reported build 2, 3 and 4, and a separate
Release configure with the option OFF reported a clean 0.27.0. Passing
the flag to a tree that does not have the option yet is an unused-cli
warning and exit 0, so the SlackBuild change is safe before 0.28.0
ships. The suite is 37 of 38, the one failure being item 136 on an
unrelated path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HFuRPtzFrSxCQjFk6tq7gD
| -rw-r--r-- | CLAUDE.md | 8 | ||||
| -rw-r--r-- | CMakeLists.txt | 39 | ||||
| -rw-r--r-- | cmake/BuildNumber.cmake | 38 | ||||
| -rw-r--r-- | docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md | 78 | ||||
| -rw-r--r-- | docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md | 47 | ||||
| -rw-r--r-- | src/CMakeLists.txt | 6 | ||||
| -rw-r--r-- | src/main.cpp | 4 | ||||
| -rw-r--r-- | src/mainwindow.cpp | 2 | ||||
| -rw-r--r-- | src/messageview.cpp | 2 | ||||
| -rw-r--r-- | src/version.h.in | 23 |
10 files changed, 196 insertions, 51 deletions
@@ -963,7 +963,13 @@ because the first four steps were treated as the whole job. a user's own config or habits need to change. 2. Bump `project(qtmaildir VERSION ...)` in `CMakeLists.txt`, the only place the version lives. Reconfigure, build, and check `./build/src/qtmaildir - --version`. + --version`. An ordinary dev build answers `X.Y.Z build N`, because + `QTMAILDIR_BUILD_NUMBER` counts rebuilds so one binary of an unreleased + version can be told from another (item 167). That is the DISPLAY version; + what a release ships is the clean one, from + `-DQTMAILDIR_BUILD_NUMBER=OFF`, which is what the SlackBuild configures and + what a tarball with no build directory produces anyway. Check the clean + form before tagging. 3. Commit as `release: X.Y.Z`, then `git tag -s vX.Y.Z -m "qtmaildir X.Y.Z"`. Tags are annotated and GPG-signed, matching every existing one. 4. `git push && git push --tags`. `origin` carries two push URLs, the personal diff --git a/CMakeLists.txt b/CMakeLists.txt index 9b4426c..e3cf82b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,12 @@ set(CMAKE_AUTORCC ON) # that ships. option(QTMAILDIR_BUILD_TESTS "Build the test suite" ON) +# A dev build counts its own rebuilds so one binary of an unreleased X.Y.Z can +# be told from another (item 167). OFF for a release: the version a release +# prints must be a clean X.Y.Z, which the release procedure checks and the +# SlackBuild builds from a tarball that carries no counter at all. +option(QTMAILDIR_BUILD_NUMBER "Number each build of a dev tree" ON) + set(QTMAILDIR_QT_COMPONENTS Widgets Svg WebEngineWidgets) if(QTMAILDIR_BUILD_TESTS) list(APPEND QTMAILDIR_QT_COMPONENTS Test) @@ -58,6 +64,39 @@ configure_file( ${CMAKE_CURRENT_BINARY_DIR}/generated/qtmaildir/version.h @ONLY) +# buildnumber.h is generated separately, and by a BUILD step rather than by +# configure_file: this whole block runs once per cmake run, so a counter +# written here would not move when the user rebuilt after a fix, which is the +# defect item 167 describes. The header always exists so version.h can include +# it unconditionally; in a release build it is empty and +# QTMAILDIR_VERSION_DISPLAY collapses to the plain version. +set(QTMAILDIR_BUILD_NUMBER_HEADER + ${CMAKE_CURRENT_BINARY_DIR}/generated/qtmaildir/buildnumber.h) + +if(QTMAILDIR_BUILD_NUMBER) + # Once now, so the header exists before the first compile of a fresh build + # directory, and then again on every build. + execute_process(COMMAND ${CMAKE_COMMAND} + -DCOUNTER_FILE=${CMAKE_CURRENT_BINARY_DIR}/build-number + -DHEADER_FILE=${QTMAILDIR_BUILD_NUMBER_HEADER} + -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/BuildNumber.cmake) + + add_custom_target(qtmaildir_buildnumber ALL + COMMAND ${CMAKE_COMMAND} + -DCOUNTER_FILE=${CMAKE_CURRENT_BINARY_DIR}/build-number + -DHEADER_FILE=${QTMAILDIR_BUILD_NUMBER_HEADER} + -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/BuildNumber.cmake + BYPRODUCTS ${QTMAILDIR_BUILD_NUMBER_HEADER} + COMMENT "Numbering this build" + VERBATIM) +else() + file(WRITE ${QTMAILDIR_BUILD_NUMBER_HEADER} + "// Release build: no counter, so QTMAILDIR_VERSION_DISPLAY is the +// plain version. Generated by the top-level CMakeLists.txt. +#pragma once +") +endif() + add_subdirectory(src) if(QTMAILDIR_BUILD_TESTS) enable_testing() diff --git a/cmake/BuildNumber.cmake b/cmake/BuildNumber.cmake new file mode 100644 index 0000000..a79f1e7 --- /dev/null +++ b/cmake/BuildNumber.cmake @@ -0,0 +1,38 @@ +# Increment the build counter and write buildnumber.h. +# +# Run with `cmake -P` as a build step, so it fires on every build rather than +# once per configure. Item 167: the version alone cannot tell one build of an +# unreleased X.Y.Z from another, and the user hand-tests unreleased builds +# daily. +# +# Expects COUNTER_FILE and HEADER_FILE on the command line. +# +# The counter lives in the BUILD directory and is deliberately not tracked: a +# committed counter would conflict on every pull and leave the tree dirty +# after every build. A fresh build directory therefore restarts at 1, which is +# honest, since it is a different build tree. + +if(EXISTS "${COUNTER_FILE}") + file(READ "${COUNTER_FILE}" current) + string(STRIP "${current}" current) +endif() + +if(NOT current MATCHES "^[0-9]+$") + set(current 0) +endif() + +math(EXPR next "${current} + 1") +file(WRITE "${COUNTER_FILE}" "${next}\n") + +# Written to a temporary and copied only if different, so a rebuild that +# changes nothing else does not force every translation unit including +# version.h to recompile... except that the number itself changes every time, +# so it always differs. That is the accepted cost of the feature: the few +# files that read the version are recompiled and relinked on every build. +file(WRITE "${HEADER_FILE}.tmp" +"// Generated by cmake/BuildNumber.cmake on every build. Do not edit, and do +// not commit: this file lives in the build directory. +#pragma once +#define QTMAILDIR_BUILD_NUMBER \"${next}\" +") +file(COPY_FILE "${HEADER_FILE}.tmp" "${HEADER_FILE}" ONLY_IF_DIFFERENT) diff --git a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md index ae6443d..b9eb8cf 100644 --- a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md +++ b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md @@ -8034,3 +8034,81 @@ ever be empty. That is real, independent of this item, and outside this repository. **Size: XS.** Done. + +## 167. No way to tell one build of an unreleased version from another + +**Observed (user, from the notes):** "we should add a dev build number to be +pushed everytime we rebuild after a fix, so that I can verify if I'm in the +correct app version." The note has sat unrecorded through several sessions; +the 2026-08-25 reconciliation is the first to pick it up. + +**Cause (verified in the code, 2026-08-25.)** The version lives in exactly one +place, `project(qtmaildir VERSION ...)`, and `src/version.h.in` interpolates +`@PROJECT_VERSION@` and nothing else. That is correct for a release and says +nothing between two of them: the string moves only when the release procedure +bumps it, so every rebuild of `0.27.0` reports `0.27.0`. The status table above +shows why it bites in practice, since most closed items since 0.27.0 read +"unreleased" and the user hand-tests each one against a binary they rebuilt +themselves. + +Both surfaces that show the version take it from the same macro, so whatever is +added reaches them at once: the window title (`mainwindow.cpp:939`), the About +dialog (`mainwindow.cpp:2449`), the placeholder pane (`messageview.cpp:547`), +`--version` and `--help` (`main.cpp`). + +**Approach.** Needs a DECISION before any code, because the two candidates fail +in opposite directions. + +A git description (`git describe --always --dirty`, or the short hash) is +accurate and self-explaining: it names the commit the binary was built from, and +a reviewer can check out exactly that. Its cost is that CMake computes it at +CONFIGURE time, so a build after a new commit reports the previous hash unless +the configure step is made to re-run, which is a custom command with a dependency +on `.git/HEAD` and the packed refs, and is the part that usually ships subtly +wrong. + +A monotonic counter always moves and needs no git, but it means nothing on its +own: build 412 does not say which fix is in it, and it differs between the user's +machine and any other, so it cannot be quoted in a report. + +**Constraints.** A release build must keep printing a clean `X.Y.Z`, since the +SlackBuild in the `my-slackbuilds` repo builds from the release tarball where +there is no git checkout at all, and the release procedure checks +`./build/src/qtmaildir --version`. Whatever is added is therefore an addition to +the string in a dev build and absent in a release one, not a change to the +version itself. + +**Decision (user, 2026-08-25): the counter.** The git description was +offered as the recommendation and was not chosen; what the user wants is to +know a rebuild happened, not which commit it was. + +**Built 2026-08-25, unreleased.** `QTMAILDIR_BUILD_NUMBER`, a cmake option ON +by default, runs `cmake/BuildNumber.cmake` as a build step: it increments a +counter and writes `buildnumber.h`, which `version.h` includes. +`QTMAILDIR_VERSION_DISPLAY` is `X.Y.Z build N` when that macro is defined and +plain `X.Y.Z` when it is not. + +Two macros, not one, and the split is the load-bearing part. +`QTMAILDIR_VERSION` stays clean and keeps the window title, `applicationVersion` +and anything that might ever compare versions; `QTMAILDIR_VERSION_DISPLAY` goes +to the three surfaces the user picked: `--version`, `--help`, the About dialog +and the placeholder pane. The window title was offered and declined, since the +number would then sit in every screenshot. + +**The counter had to be a BUILD step, not `configure_file`.** That is the whole +reason this is not two lines: `configure_file` runs once per cmake run, so a +counter interpolated into `version.h.in` sits still across exactly the rebuilds +this item exists to distinguish. `version.h.in` therefore includes a second +generated header rather than carrying the number itself. + +The counter file lives in the build directory and is not tracked, so it cannot +conflict on a pull or dirty the tree; a fresh build directory restarts at 1, +which is honest, because it is a different build tree. A release build passes +`-DQTMAILDIR_BUILD_NUMBER=OFF` and the header is written empty. + +**Verified by running it**, since none of this is reachable from a C++ test: +three consecutive builds reported `build 2`, `build 3`, `build 4`, and a +separate Release configure with the option OFF reported a clean `0.27.0`. The +suite is 37 of 38, the one failure being item 136 on an unrelated path. + +**Size: XS**, as sized. diff --git a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md index 6c29fe0..3cdc71a 100644 --- a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md +++ b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md @@ -240,7 +240,7 @@ taking that too literally. | 164 | A draft this application saved keeps `inbox` | defect | S | open, 2026-08-25, **cause corrected 2026-08-25**. The first diagnosis blamed a missing drafts helper and was WRONG: `NOT_ARRIVALS` in `qtmaildirconf.py` is `("sent", "drafts")`, the folder list includes every account's drafts folder, and `notmuch count` confirms the carve-out query MATCHES the affected draft. The carve-out is scoped to `tag:new`, and the draft carries `inbox` while `tag:new` is 0, so it was never in scope when the hook ran. Measured separately: an mbsync-style rename does NOT re-add `new.tags`, so the retag theory is out too. What remains unestablished is WHICH pass tagged it; establish that before writing code | | 165 | A draft gets a new Message-ID on every autosave | enhancement | ? | open, 2026-08-25, found while hand-testing 163 and 164. `MessageBuilder::build()` generates an id unconditionally and every autosave calls it, so each revision is a distinct MESSAGE to notmuch and to the server rather than a new version of one. Invisible while the file is replaced correctly, which item 163's fix restores; it is what turned that fork into two messages rather than one duplicated file. Needs a DECISION on what a draft's identity is before any code: a stable id reused at send, a stable id discarded at send, or the status quo. Neither `ComposeContext` nor `OutgoingMessage` has a field to carry an id, so it is not a changed call site | | 166 | Mail you send to your own other account loses `inbox` | defect | S | open, found 2026-08-25. Wholly this repo's: the hooks live in `assets/hooks/` with their own suites, and the live `post-new` symlinks to them | -| 167 | No way to tell one build of an unreleased version from another | enhancement | XS | open, found 2026-08-25 from the notes, unrecorded until now. `src/version.h.in` interpolates `PROJECT_VERSION` alone, so every build between two releases reports the same string and a rebuilt binary cannot be told from the one it replaced. The user runs unreleased builds daily, which is when it matters. Needs a DECISION on the source: the git describe/short hash (accurate, needs the build to re-run cmake to pick up a new commit) or a monotonic counter (always moves, means nothing on its own) | +| 167 | No way to tell one build of an unreleased version from another | enhancement | XS | **done 2026-08-25**, unreleased. The user chose a counter over a git description: `QTMAILDIR_BUILD_NUMBER`, a cmake option ON by default, increments a counter in the BUILD directory on every build and writes `buildnumber.h`. `QTMAILDIR_VERSION_DISPLAY` carries it; `QTMAILDIR_VERSION` stays clean and is what the window title, `applicationVersion` and the release procedure use | Sizes are rough: XS under an hour, S a sitting, M a session. @@ -1519,48 +1519,3 @@ id and a draft of a reply carries both. - Item 163's fix stands on its own and this does not block it: the file is replaced correctly now, so the fork this would have mitigated no longer happens by that route. - -## 167. No way to tell one build of an unreleased version from another - -**Observed (user, from the notes):** "we should add a dev build number to be -pushed everytime we rebuild after a fix, so that I can verify if I'm in the -correct app version." The note has sat unrecorded through several sessions; -the 2026-08-25 reconciliation is the first to pick it up. - -**Cause (verified in the code, 2026-08-25.)** The version lives in exactly one -place, `project(qtmaildir VERSION ...)`, and `src/version.h.in` interpolates -`@PROJECT_VERSION@` and nothing else. That is correct for a release and says -nothing between two of them: the string moves only when the release procedure -bumps it, so every rebuild of `0.27.0` reports `0.27.0`. The status table above -shows why it bites in practice, since most closed items since 0.27.0 read -"unreleased" and the user hand-tests each one against a binary they rebuilt -themselves. - -Both surfaces that show the version take it from the same macro, so whatever is -added reaches them at once: the window title (`mainwindow.cpp:939`), the About -dialog (`mainwindow.cpp:2449`), the placeholder pane (`messageview.cpp:547`), -`--version` and `--help` (`main.cpp`). - -**Approach.** Needs a DECISION before any code, because the two candidates fail -in opposite directions. - -A git description (`git describe --always --dirty`, or the short hash) is -accurate and self-explaining: it names the commit the binary was built from, and -a reviewer can check out exactly that. Its cost is that CMake computes it at -CONFIGURE time, so a build after a new commit reports the previous hash unless -the configure step is made to re-run, which is a custom command with a dependency -on `.git/HEAD` and the packed refs, and is the part that usually ships subtly -wrong. - -A monotonic counter always moves and needs no git, but it means nothing on its -own: build 412 does not say which fix is in it, and it differs between the user's -machine and any other, so it cannot be quoted in a report. - -**Constraints.** A release build must keep printing a clean `X.Y.Z`, since the -SlackBuild in the `my-slackbuilds` repo builds from the release tarball where -there is no git checkout at all, and the release procedure checks -`./build/src/qtmaildir --version`. Whatever is added is therefore an addition to -the string in a dev build and absent in a release one, not a change to the -version itself. - -**Size: XS** once the decision is made. diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 700b185..4d9dbaa 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -44,6 +44,12 @@ target_include_directories(qtmaildir_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${NOTMUCH_INCLUDE_DIR} ${CMAKE_BINARY_DIR}/generated/qtmaildir) +# The counter target rewrites buildnumber.h, which version.h includes, so the +# library must not start compiling before it has run. +if(TARGET qtmaildir_buildnumber) + add_dependencies(qtmaildir_lib qtmaildir_buildnumber) +endif() + target_link_libraries(qtmaildir_lib PUBLIC Qt6::Widgets Qt6::Svg Qt6::WebEngineWidgets PkgConfig::GMIME ${NOTMUCH_LIBRARY} PkgConfig::CMARK_GFM diff --git a/src/main.cpp b/src/main.cpp index 2ed8057..a7908d0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -43,7 +43,7 @@ int main(int argc, char *argv[]) for (int i = 1; i < argc; ++i) { if (std::strcmp(argv[i], "--version") == 0 || std::strcmp(argv[i], "-v") == 0) { - std::printf("qtmaildir %s\n", QTMAILDIR_VERSION); + std::printf("qtmaildir %s\n", QTMAILDIR_VERSION_DISPLAY); return 0; } if (std::strcmp(argv[i], "--help") == 0 @@ -59,7 +59,7 @@ int main(int argc, char *argv[]) "Configuration: ~/.config/qtmaildir/qtmaildir.conf\n" "qtmaildir reads a notmuch-indexed Maildir. It does no network\n" "protocol work: fetching and sending are external commands.\n", - QTMAILDIR_VERSION); + QTMAILDIR_VERSION_DISPLAY); return 0; } } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 5845922..6b48880 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -2446,7 +2446,7 @@ void MainWindow::showAbout() "version 2.</p>" "<p>Developed with AI assistance. All code is reviewed, " "tested and curated by the maintainer.</p>") - .arg(QStringLiteral(QTMAILDIR_VERSION))); + .arg(QStringLiteral(QTMAILDIR_VERSION_DISPLAY))); auto *link = new QLabel( QStringLiteral("<a href='https://danix.xyz/qtmaildir'>" diff --git a/src/messageview.cpp b/src/messageview.cpp index 86e40eb..eb0dccd 100644 --- a/src/messageview.cpp +++ b/src/messageview.cpp @@ -544,7 +544,7 @@ void MessageView::showPlaceholder( // uses it: a style sheet or a themed parent can give this pane different // colours from the application. setDocument(HtmlBuilder::buildPlaceholder( - helpers, QStringLiteral(QTMAILDIR_VERSION), + helpers, QStringLiteral(QTMAILDIR_VERSION_DISPLAY), HtmlBuilder::brandPaletteFrom(palette()))); } diff --git a/src/version.h.in b/src/version.h.in index 8d76a3a..743adc5 100644 --- a/src/version.h.in +++ b/src/version.h.in @@ -25,3 +25,26 @@ #define QTMAILDIR_VERSION_MINOR @PROJECT_VERSION_MINOR@ #define QTMAILDIR_VERSION_PATCH @PROJECT_VERSION_PATCH@ #define QTMAILDIR_VERSION "@PROJECT_VERSION@" + +/// The version as shown to a person, which in a DEV build carries the build +/// number and in a release build is exactly QTMAILDIR_VERSION. +/// +/// Two separate macros deliberately. The release procedure checks `--version` +/// against a clean X.Y.Z, the SlackBuild builds from a release tarball where +/// no build counter exists, and the window title is a poor place for a number +/// that changes on every rebuild. Anything comparing versions uses +/// QTMAILDIR_VERSION; anything a person reads to answer "which build am I +/// running" uses this one. +/// +/// buildnumber.h is generated at BUILD time, not here: this file is written +/// by configure_file(), which runs once per cmake run, so a counter +/// interpolated into it would sit still across every rebuild, which is the +/// entire thing item 167 is about. It defines QTMAILDIR_BUILD_NUMBER only in +/// a dev build. +#include "buildnumber.h" + +#ifdef QTMAILDIR_BUILD_NUMBER +# define QTMAILDIR_VERSION_DISPLAY QTMAILDIR_VERSION " build " QTMAILDIR_BUILD_NUMBER +#else +# define QTMAILDIR_VERSION_DISPLAY QTMAILDIR_VERSION +#endif |
