Releasing textweaver
A release is a git tag vX.Y.Z[-pre] and a GitHub release with these files. Since beta 1 there is one package per OS and CPU architecture, and since beta 3 each holds one program, textweaver (the app by default, and the terminal program with --tw or when started as tw), with tw beside it and the complete documentation:
- the Windows package,
textweaver-VERSION-windows-x86_64.zip; - the macOS package,
textweaver-VERSION-macos-universal.zip(textweaver.appbesidetw, Apple silicon and Intel); - the Linux AppImages,
textweaver-VERSION-linux-x86_64.AppImageandtextweaver-VERSION-linux-aarch64.AppImage, each with its.zsyncfile for delta updates (since beta 3 without the-gui.AppImage.zsynccopy that the app AppImages of 0.1.0-alpha.9 and earlier looked for); - the Linux tarballs,
textweaver-VERSION-linux-x86_64.tar.gzandtextweaver-VERSION-linux-aarch64.tar.gz, for systems without FUSE; SHA256SUMS.txt, covering every package.
cargo xtask release-assets holds this list, and the release workflow checks each build folder and the finished release against it.
The Release workflow (.github/workflows/release.yml) builds the packages, attests their build provenance, and writes the checksums. Releases before 1.0 are marked as pre-releases.
Steps
- Listen, if you want to (optional). A release does not wait for a listening check (the owner's decision, Wednesday, September 30, 2026). Tests that fake the engine cannot hear a silent one, so a listen on real hardware is still worth doing when speech has changed a lot. When you do one, go through the listening checklist below and record it:
cargo xtask release 0.1.0-beta.3 --listened
This writes today's date and the version on the "Last listening check" line of this guide, and changes nothing else. The release prints a note when that line is old or for another version, and goes on.
- Prepare. Before you start, run the checks CI runs, locally:
scripts/dev-check.shon Linux or macOS (--dockerfor the full Linux set), orscripts\dev-check.ps1on Windows. They include the link check and the site data check. Then, onmain, with a clean tree and CI green, try the release without changing anything:
cargo xtask release 0.1.0-beta.3 --dry-run
It prints the date it will use, the files it will change, and the checks it will run. Right after a release, [Unreleased] in CHANGELOG.md is empty; a dry run lists that as a problem, the same way it lists the other things a real release would stop for, instead of stopping itself. Then run it for real:
cargo xtask release 0.1.0-beta.3
This:
- stops unless the tree is clean and on
mainand the changelog is grouped by area (below), and notes an old or missing listening check without stopping; - sets
versionin[workspace.package]in the rootCargo.tomland runscargo update -w; - turns
## [Unreleased]inCHANGELOG.mdinto## [0.1.0-beta.3] - YYYY-MM-DD, keeps an empty[Unreleased]above it, and adds the release link. The date comes from the machine's clock in local time, and the weekday is computed and printed so you can check it. It is never typed in; - updates the version examples in this guide,
docs/install.md, the README, the crate map (docs/site/architecture.html), and the workflows, and lists every other line that still names the old version, so a file that should follow the release is seen in the dry run. In the files it updates, a line that records history keeps its version: a line that ends with the comment<!-- history -->, which Markdown does not show, and the "Last listening check" line, which only--listenedwrites. When you write a line that names a past release as history, such as "the sizes recorded for 0.1.0-beta.1 are the first", put it on a line of its own and end it with that comment; - runs the checks CI runs: fmt, clippy, the tests,
cargo xtask keyboard --check, andcargo xtask deps --check(--no-checksskips them, for a rerun after a failure you have fixed); - commits "Release 0.1.0-beta.3" and makes the annotated tag
v0.1.0-beta.3. It pushes nothing.
Before a release, also run cargo xtask notices (it needs cargo install --locked cargo-about) and commit THIRD-PARTY-NOTICES.md if it changed. CI fails when it is out of date.
The changelog, grouped by area. During development, contributors may add their CHANGELOG.md lines under a scratch heading of their own, to keep unrelated changes from colliding. Before a release, move those lines under area headings (reading and speech, documents, writing, the GUI, languages, packages) and add a short summary at the top of the section. cargo xtask release names any scratch heading it still finds in [Unreleased] and stops.
- Try the packages. Start the
Releaseworkflow from the Actions tab onmain, with the tag left empty. That is a dry run: it builds and checks every package, keeps them as workflow artifacts, and creates no release, tag, or attestation. It also works on a pushed branch, to try a change to the packaging before it merges:
gh workflow run release.yml --ref BRANCH
Download the artifacts from the run's page (or gh run download RUN_ID), and start each package once: the app with a screen reader on Windows, and the terminal reader on each system you use. This is optional and never holds a release; it is most useful when the packages change.
- Push. Push the commit, then the tag:
git push origin main
git push origin v0.1.0-beta.3
Pushing the tag starts the Release workflow:
- Create the release. Checks that the version in
Cargo.tomlmatches the tag, then creates the GitHub release as a pre-release, with notes taken from the matchingCHANGELOG.mdsection. - Windows package (on
windows-latest) and macOS package (onmacos-14), in parallel. Each runscargo xtask distonce, which builds the program, then checks the package: the files inside (textweaver,tw, the 32-bit engine host on Windows, the notices and license files; on Windows exactly three programs),twwithtools/package-smoke.sh(which also runstextweaver --tw --versionandtextweaver text), and the app withtextweaver --versionand--screenshot, which draws the app's interface on the CPU with no display. On Windows it also checks thattw.exepasses on the exit status of a usage error. On macOS it checks thattwandtextweaverare links to the program intextweaver.app, and reads a document silently with the paced backend in a background window, then closes. On Windows the checks starttextweaver.exewithStart-Process -Wait, since it is a GUI-subsystem program. Then it attests the package's build provenance and uploads it to the release. - Linux AppImage and tarball, x86_64 and aarch64 (on
ubuntu-latestandubuntu-22.04-arm, in parallel with the others). Each runscargo xtask appimagein thedocker/appimageimage (Ubuntu 22.04), thendocker/appimage/test-distros.sh, which runs both packages on Debian stable and Fedora, and on Arch for x86_64 (Arch has no official arm64 image):tw --version,tw backendswithout and with espeak-ng,tw text,textweaver --version,--installand--uninstall, andinstall-linux.sh --releasewith each package. It checks the app in the same packages (textweaver --versionand--screenshot). Then it attests and uploads the AppImage, its.zsyncfile, and the tarball. The app is not yet run on other Linux distributions. -
SHA256SUMS.txt. Once all the packages are uploaded, one job writes the checksums of every package on the release and attests the checksum file. The package jobs never write checksums, so they cannot race.
-
Check. Read the release page. It should have every package and
SHA256SUMS.txt, with the pre-release flag set. Anyone can check where a package was built:
gh attestation verify textweaver-0.1.0-beta.3-windows-x86_64.zip --repo leavesofgrass/textweaver
- Record the package sizes. Once every package is on the release:
cargo xtask release 0.1.0-beta.3 --sizes
It reads each package's size from the release (gh release view, read only), rewrites xtask/package-sizes.toml with them and clears its notes, adds a "Package sizes" list to the version's section of CHANGELOG.md, and writes that section to target/release-notes-0.1.0-beta.3.md. Commit the two files. To show the sizes on the release page too, run the gh release edit command it prints. --sizes-from DIR reads the package files in a folder instead, such as a dry run's downloaded artifacts. See Package sizes.
Listening checklist
Optional. When you want to listen, do this on the machine you use every day, with Eloquence, SAPI 5, and Piper (and DECtalk if it is installed). Then record it with cargo xtask release VERSION --listened, which rewrites this line from the machine's clock:
Last listening check: 2026-09-29 (Tuesday, September 29, 2026), for 0.1.0-alpha.9.
- Write the samples.
cargo xtask listen
It writes target/listen/<engine>.wav, <engine>-fast.wav (400 words per minute), and <engine>-low.wav (4 semitones down) for every engine on this machine, each with word-level subtitles (.srt) beside it. An engine that is not installed is reported and skipped. It plays nothing. --engine piper limits it to one engine.
- Listen to each file. For each engine:
- The heading, "Chapter 3: The Library", is read, and "Chapter 3" is not "Chapter three colon".
- "9:00 a.m." is a time, "$1.50" is money, and "Dr. Okafor" is "Doctor Okafor".
- The question rises at the end.
- The fast file is still clear; the low file is lower, not slower.
-
No clicks, no cut-off first or last word, and no long silence between sentences.
-
Check the highlight timing. Open a
.srtfile in a player that shows subtitles (or read the times): each word's time should match when you hear it, within about a tenth of a second. -
In the reader. Run
textweaveron a document and, with each engine: - Read, pause, resume, and stop.
- Move by sentence and heading while reading.
- Change the rate and pitch; then choose another voice (Alt+V) and back. The first voice comes back with its own rate and pitch.
- In the voice manager (Alt+V), move the language and engine filters with Enter on their rows.
- In the voice manager, press Alt+End on a voice of this engine and on one of another engine: each says its name and a sample in its own voice, and the voice in use stays as it was.
-
In the GUI's voice manager (Ctrl+Alt+V), Tab through the Language and Engine buttons, the list, and Use voice, Preview, Favorite, Remove, and Close: each is read with its name, role, and key. Press Engine and check the list and the button's name change.
-
Piper. With a Piper voice installed:
tw backendslistspiperas available and says it supports word highlighting.- The first word of a long paragraph starts within about a third of a second of pressing Read.
- In the voice manager, "Fetch the Piper voice list" asks before downloading, and a voice to download says its size and its license before asking. Say no once, then yes once, and check the voice appears in
<data>/piper/voices/. -
Remove the downloaded voice with Delete; it asks first.
-
Dictation. With the Whisper model in
<data>/whisper/rten/base.en(dictation guide), runtw dictate --timings, say a sentence, and press Enter. Check the text, and write down the time from Enter to the text. -
Braille: pending. The first session with a Braille display (a 40-cell Mantis Q40 through NVDA and JAWS) has not been held yet. Its items join this list after it. Until then, go through the checklist for the Mantis Q40 in the screen reader guide, and write down what the display showed.
Write down what you heard in the release notes' testing section, including anything odd.
What the packages hold
cargo xtask dist builds with the dist profile (the release profile with fat LTO, and symbols stripped) and writes the package to target/dist/. It builds the program, the textweaver binary of textweaver-xilem (which runs textweaver-cli's library for the terminal), and on Windows the launcher, textweaver-launcher's tw-launcher, installed as tw.exe, in a cargo run of its own, so it links only the standard library. cargo xtask gui-dist is another name for the same task. The C runtime is linked statically on Windows, so the package does not need the Visual C++ redistributable. Each package holds:
textweaver, the program (on macOS insidetextweaver.app): the app, the terminal program with--tw, and the native engine hosts with--engine-host; with the app's guideGUI.mdat the top and Xilem's license;tw, the terminal program's name: a link totextweaveron Linux, a link to the program intextweaver.appon macOS (with atextweaverlink beside it too), and on Windowstw.exe, a console launcher of a few hundred kilobytes that runstextweaver.exe --twwith the same arguments and standard handles, waits, and passes its exit status on;- on Windows, the 32-bit engine host,
textweaver-host-x86.exe, and the IBMTTS community dictionaries; - on Linux, the IBMTTS community dictionaries, and the menu entries for the app (
textweaver.desktop) and the terminal reader (textweaver-terminal.desktop) and the icon undershare/. The program is built with Omnivox, speech-dispatcher, and espeak-ng; espeak-ng is loaded when the program starts, if it is installed, so the same binary works without it, and Fontconfig is loaded only when the app starts; QUICKSTART.md,README.md,LICENSE,NOTICE(the copyright notice),CHANGELOG.md, andINSTALL.mdat the top;- in
docs/, the documentation index, every user guide it lists under "For users", and the offline interactive pages indocs/site/, each at its path in the repository; - the platform's helper scripts (doctor, speech check, update) and their README;
THIRD-PARTY-NOTICES.md, and underlicenses/: each bundled font'sOFL.txt, SCOWL'sCopyright, and the IBMTTS dictionaries' license.
cargo xtask dist fails if a program, an engine host, or one of the notices and licenses is missing from the staged folder, and on Windows if the folder holds any program but textweaver.exe, tw.exe, and textweaver-host-x86.exe.
A missing guide warns, and the package still builds. The documentation is staged by one helper, stage_user_docs in xtask/src/dist.rs, which cargo xtask dist and appimage call. When a guide the index lists, the index itself, or docs/site/ is missing, the build leaves it out, prints one line per missing file, such as "Warning: the package lacks the guide docs/reading.md.", and goes on. When GITHUB_STEP_SUMMARY is set, as in every GitHub Actions step, the same lines are added to the job summary, so the release run shows them. Read the summary before you publish: a warning there means a package went out without a guide. With nothing missing, nothing is printed.
The app in the package
The program's part of the build is in xtask/src/gui_dist.rs. It builds the textweaver binary of textweaver-xilem with the platform's speech engines (espeak-ng, speech-dispatcher and Omnivox on Linux; Omnivox elsewhere), for each one the crate declares as a feature (it passes each to textweaver-cli too), and names any engine it leaves out because the crate has no such feature. On macOS it puts the program in textweaver.app (signed ad hoc), built for the Mac's own architecture, or with --universal for Apple silicon and Intel joined with lipo, as the release does, and the package folder is zipped with ditto, which keeps the tw and textweaver links.
The screenshot harness stays in the package for now. The app's default screenshot feature (--screenshot and --review-screenshots, drawn with Vello's CPU renderer, image, and oxipng) is the only way the release workflow can check that the packaged app draws its interface: its runners have no GPU Vello can use, and the Linux check runs with no display. So the checks on all three systems run textweaver --screenshot. cargo xtask dist --no-screenshot builds the package without it, with every other default feature; it can become the default once the release checks no longer need the harness. Review screenshots come from a developer build either way.
Releases up to 0.1.0-alpha.9 had the app in packages of its own, named like the terminal packages with -gui at the end. Those are not made any more, and since beta 3 neither are the -gui copies of the Linux .zsync files that let those AppImages update.
The Linux packages
cargo xtask appimage stages the Linux package as cargo xtask dist does, writes the tarball, and then wraps the same folder in an AppImage with appimagetool. The folder sits whole under usr/lib/textweaver/ inside the AppImage, so the programs find the hosts and dictionaries beside them, as in the tarball. scripts/linux/AppRun is the entry point: it runs textweaver, which opens the app by default and runs the terminal program with --tw or --cli first; started through a link named tw (or with --textweaver first, as menu entries from before beta 3 do) it runs the program as tw. --gui and --app are accepted and ignored. It offers --install and --uninstall, which link textweaver and tw, add menu entries for the app and the terminal reader, and remove the textweaver-gui link and entry that earlier versions made. The AppImage carries gh-releases-zsync update information pointing at the newest release or pre-release, under the same name the terminal AppImages always had, so they update to the single image. Since beta 3 no -gui.AppImage.zsync copy is written for the app AppImages of 0.1.0-alpha.9 and earlier.
Build on an old glibc, so the packages run on older distributions. The docker/appimage image is Ubuntu 22.04 (glibc 2.35), with Rust from rustup, the AppImage tools, and, for the GUI build, libfontconfig1-dev (yeslogic-fontconfig-sys needs its headers). docker/appimage/fetch-tools.sh downloads appimagetool 1.9.1 and the type 2 runtime 20251108, for x86_64 or aarch64, from their GitHub releases, and checks each against the SHA-256 digest GitHub publishes for it; a changed file stops the build. The image builds for the machine it runs on, so the aarch64 packages are built on an arm64 machine: the release workflow uses GitHub's ubuntu-22.04-arm runner. To build locally on any system with Docker:
cargo xtask appimage --docker
The packages land in target/dist/. To check them on Debian, Fedora, and Arch, as the release job does (it needs Docker and bash; ARCH=aarch64 checks the aarch64 packages, on an arm64 machine, with debian:stable fedora:latest after the folder):
bash docker/appimage/test-distros.sh target/dist
The AppImage is not signed; its checksum is in SHA256SUMS.txt, and its build provenance is attested. It needs the system's ALSA library (libasound.so.2), which every desktop has, and warns when it is missing.
Package sizes
Every package has a size budget. xtask/package-sizes.toml records each package's size in bytes at the last release, named without the version (windows-x86_64.zip, linux-x86_64.AppImage). After writing a package, cargo xtask dist (or gui-dist) and appimage print one line for it, meaning first:
Size within budget:with its size and the change in percent from the last release;Size over budget, with a note:and the note;Size over budget:when it grew more than 10 percent with no note. The command then fails, after the package is written, so the release workflow stops before it uploads.
A package with no recorded size (a new platform, or a Mac build that is not --universal) is printed and passes.
When a package is meant to grow, say why under [notes] in the file, in the same change:
[notes]
"windows-x86_64.zip" = "Opus encoding in process for Export audio"
A note named "all" covers every package.
The sizes recorded for 0.1.0-beta.1 are the first for the single packages; they replace the alpha.9 terminal and app packages, which were two downloads.
The release step (cargo xtask release VERSION --sizes, step 6 above) writes the new sizes and clears the notes, so each note covers one release. The sizes also go into the release notes, as a "Package sizes" list in the version's section of CHANGELOG.md. MB there, as in the release workflow's summaries, is 1,048,576 bytes.
Building a package by hand (fallback)
If a package job fails, build that package locally and upload it. On Windows this needs the MSVC toolchain and the 32-bit target (rustup target add i686-pc-windows-msvc):
cargo xtask dist
On macOS:
cargo xtask dist --universal
Then upload it and refresh the checksums. tools/release-upload.sh does both (it needs gh, logged in):
tools/release-upload.sh v0.1.0-beta.3 target/dist/textweaver-0.1.0-beta.3-windows-x86_64.zip
If the release does not exist yet, the script creates it. A package uploaded this way has no provenance attestation.
To rerun the workflow for an existing tag, start Release from the Actions tab and enter the tag.
Notes
- macOS signing. The macOS binaries are universal (built with
lipo) and signed ad hoc (codesign -s -). They are not notarized.docs/install.mdtells users how to get past Gatekeeper. Notarizing needs an Apple Developer ID. When there is one, addcodesign --options runtimewith that identity andxcrun notarytool submit --waitto the workflow. - No engines are bundled. No speech engines are in the packages: no Eloquence, no DECtalk, no voices. The packages hold only textweaver's own programs and hosts, and the CC0 IBMTTS dictionaries.
- Linux. The AppImage and the tarball are built for x86_64 and aarch64. Other systems build from source with
scripts/install-linux.sh.
See also
- Installing textweaver: what users do with the packages.
- CONTRIBUTING.md: the checks and the commit style.
- CHANGELOG.md: the release notes come from here.
- ADR-0012: The engine host:
cargo xtask hostsand host versioning. - Documentation index