feat(release): binaries on tag — Distribution Stage 1

.github/workflows/ had exactly one workflow and it was test-only: no
release job, no artifact upload, no tags-to-binaries path. Installing
pmacs meant `git clone` plus knowing the feature-flag matrix.
COHERENCE.md §17 grades this "missing — zero release machinery exists";
this moves it to Partial and completes journey step 1.

Scope is one stage: binaries when a `v*` tag is pushed, attached to a
GitHub Release. Channels, rollback, update-in-place, signing, RHEL 9 and
Intel macOS are out of scope and named in the framing's §5.

WHAT SHIPS: pmacs and pmacs-gpu, both at 1.1.0, CRDT-enabled, co-located
in one archive, with SHA256SUMS. pmacs-protocol stays at 1.0.0 — it is
the wire crate and versions on its own schedule.

THE VERSION BUMP EXPOSED A REAL DEFECT, and it is the reason this PR
touches src/ at all. `InstanceIdentity::for_running_process` is defined
in pmacs-protocol and expanded `env!("CARGO_PKG_VERSION")` THERE. `env!`
expands in the crate being compiled, so the field documented as "Pmacs
version string" carried the PROTOCOL crate's version. That identity
reaches Lua as `pmacs.instance.identity()` and goes on the wire in
`Hello`, so a 1.1.0 release would have told every attached frontend it
was 1.0.0.

Nothing could have caught it earlier. Three tests assert
`id.pmacs_version == env!("CARGO_PKG_VERSION")` evaluated in the pmacs
crate — the correct assertion — but while both crates read 1.0.0 they
compared the same number reached by two different paths and COULD NOT
FAIL. Deciding to hold pmacs-protocol at 1.0.0 while moving pmacs is
what made them discriminating; all three failed on the bump. The version
is now a parameter so `env!` expands in the caller's crate. A test can
be correct and still prove nothing when the two things it compares are
equal for a reason unrelated to the code under test.

TWO LAYERS OF BINARY EXCLUSION, and layer 2 is load-bearing —
demonstrated, not argued. Cargo auto-discovers src/bin/*.rs, so a
release build can produce five binaries and three must never ship
(pmacs-audit is a contributor tool; pmacs_fake_lsp and pmacs_fake_mcp
are test fixtures). Layer 1 names explicit --bin targets. Layer 2 stages
an explicit asset list, and building this branch produced exactly the
case it guards: after building ONLY --bin pmacs and -p pmacs-gpu,
target/release still held all three forbidden binaries, left by an
earlier `cargo test --release`. Swatinem/rust-cache restores that kind
of directory in CI. An implementation trusting layer 1 and archiving the
directory would have published a fake language server in the first
release.

The three archive assertions are bite-verified: a smuggled
pmacs_fake_lsp, a missing pmacs-gpu, and a cleared executable bit are
each caught, with the honest archive passing.

THE GLIBC FLOOR IS ASSERTED, NOT TRUSTED. Pinning ubuntu-22.04 sets the
floor at 2.35 (Ubuntu 22.04, Debian 12 — NOT RHEL 9 at 2.34, which needs
a container or cross-build and is parked). But a pinned runner proves
nothing about the artifact, and the failure surfaces as a bare
`GLIBC_2.39 not found` on a user's machine with no clue which commit
caused it. The build reads versioned-symbol requirements out of the
binary and fails above the floor, so switching to ubuntu-latest fails in
CI instead of shipping. Bite-verified both directions on a glibc 2.44
host. Both runners are pinned; macos-latest would drift the minimum
supported macOS with no commit to point at.

Preflight runs before any build: the tag must match the root crate
version (stripping a prerelease suffix, so v1.1.0-rc.1 and v1.1.0 both
match 1.1.0), and the tagged commit must be an ancestor of main. Both
catch mistakes that are cheap now and expensive once a public URL
exists. The suite is not re-run — CI already tested the commit — but
nothing otherwise enforced that a tag points at a tested one.

Verified: fmt, diff-check, clippy with and without crdt, --lib 1896,
--lib --features crdt 2081, pmacs-protocol 19, m4 149, required GPU 221,
and the full serialized crdt sweep at 3,715 passed / 0 failed / 30
ignored — identical to the pre-change baseline, so the protocol
signature change broke nothing. Archive staging, contents, executable
bits and both --version outputs were exercised against a real release
build locally.

No release is cut by this PR. Per the framing's §7 the RC is tagged
after merge, from the merge SHA.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Levi Neuwirth 2026-08-01 14:40:47 -04:00
parent 2ed2596046
commit 84b1620e7e
No known key found for this signature in database
10 changed files with 534 additions and 15 deletions

362
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,362 @@
name: Release
# Distribution Stage 1 — binaries on tag. See
# docs/distribution-stage1-framing.md.
#
# Scope is deliberately one stage: build the two shipped binaries when a
# `v*` tag is pushed and attach them to a GitHub Release. There are no
# channels, no rollback, no update-in-place, no signing. Those are named
# as out of scope in the framing's §5 rather than forgotten.
#
# Nothing here runs on a branch push or a pull request. A tag is the
# only trigger, because a release built from anything else would publish
# artifacts for a commit nobody reviewed as a release.
on:
push:
tags:
- 'v*'
# A release must never race itself. Two tags pushed close together would
# otherwise both mutate the same release page.
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
env:
CARGO_TERM_COLOR: always
permissions:
contents: write
jobs:
# ---------------------------------------------------------------
# Preflight runs BEFORE any build, because both of its checks catch
# mistakes that are cheap now and expensive once artifacts exist: a
# published release is a public URL, and unpublishing one is not the
# same as never having published it.
# ---------------------------------------------------------------
preflight:
name: Preflight (tag/version, ancestry)
runs-on: ubuntu-22.04
timeout-minutes: 10
outputs:
version: ${{ steps.check.outputs.version }}
prerelease: ${{ steps.check.outputs.prerelease }}
steps:
# Full history: the ancestry check below cannot run on a shallow
# clone, and the default checkout depth is 1.
- uses: actions/checkout@v4
with:
fetch-depth: 0
- id: check
name: Tag must match the root crate version, and be on main
run: |
set -euo pipefail
tag="${GITHUB_REF_NAME}"
# Strip the leading v, then strip any prerelease suffix:
# v1.1.0-rc.1 and v1.1.0 must both match a crate version of
# 1.1.0. Cargo has no place to record "rc.1", so requiring the
# manifest to carry it would make prereleases impossible.
version="${tag#v}"
base="${version%%-*}"
# Read the ROOT package version specifically. A grep for
# `^version` across the workspace would match whichever
# manifest sorted first, and pmacs-protocol deliberately holds
# a different number.
manifest=$(cargo metadata --no-deps --format-version 1 \
| python3 -c 'import json,sys; print(next(p["version"] for p in json.load(sys.stdin)["packages"] if p["name"]=="pmacs"))')
echo "tag=$tag base=$base manifest=$manifest"
if [ "$base" != "$manifest" ]; then
echo "::error::tag $tag implies version $base but the pmacs crate is $manifest." \
"Bump Cargo.toml or fix the tag; publishing this would ship a binary whose" \
"--version disagrees with its release."
exit 1
fi
# A tag on a branch is the realistic mistake. Re-running the
# test suite here would be duplicated cost -- CI already tested
# this commit -- but nothing otherwise enforces that the tagged
# commit is one CI ever saw on main.
if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
echo "::error::tagged commit $GITHUB_SHA is not an ancestor of origin/main." \
"Releases are cut from main."
exit 1
fi
# Anything with a suffix (-rc.1, -beta) is a prerelease. The
# framing requires the RC to be marked so it cannot be mistaken
# for the real thing on the releases page.
if [ "$version" = "$base" ]; then
echo "prerelease=false" >> "$GITHUB_OUTPUT"
else
echo "prerelease=true" >> "$GITHUB_OUTPUT"
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
# ---------------------------------------------------------------
# Both runners are PINNED, never `-latest`. Two different reasons,
# both about a silent choice:
#
# * ubuntu-22.04 sets the glibc floor at 2.35, which covers Ubuntu
# 22.04 and Debian 12. `ubuntu-latest` (24.04, glibc 2.39) would
# silently produce binaries that fail to load on both with a bare
# `GLIBC_2.39 not found`. Note the floor does NOT reach RHEL 9
# (glibc 2.34) -- that needs a container or cross-build and is
# parked in the framing's §5.
#
# * macos-15 is arm64. `macos-latest` drifts, so a future runner
# change could move the minimum supported macOS with no commit in
# this repository to point at.
# ---------------------------------------------------------------
build:
name: Build ${{ matrix.target-label }}
needs: preflight
runs-on: ${{ matrix.os }}
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-22.04
target-label: linux-x86_64
- os: macos-15
target-label: macos-arm64
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
# CRDT is not optional. `run_gpu` refuses outright without it
# ("--gpu requires pmacs built with --features crdt"), and
# InstanceCapabilities::default advertises multi_frontend /
# crdt_replica / semantic_render only under the feature. A
# non-CRDT release would ship an editor that cannot use the GPU
# frontend shipped beside it.
#
# EXPLICIT --bin TARGETS, layer 1 of 2. Cargo auto-discovers
# src/bin/*.rs, so a plain `--workspace` release build also
# produces pmacs-audit, pmacs_fake_lsp and pmacs_fake_mcp -- the
# last two being test fixtures. Naming targets keeps them from
# being built at all.
- name: Build the shipped binaries only
run: |
set -euo pipefail
cargo build --release --bin pmacs --features crdt
cargo build --release -p pmacs-gpu
# EXPLICIT STAGED ASSET LIST, layer 2 of 2. Layer 1 alone is not
# enough: a cached target/release can still hold binaries from an
# earlier build, and archiving that directory would publish them.
# Copying named files into a clean staging dir makes the archive's
# contents a decision rather than a directory's residue.
#
# The two binaries are staged into the SAME directory on purpose.
# `pmacs --gpu` prefers a co-located pmacs-gpu and only then falls
# back to a PATH lookup, so co-location is what makes an unpacked
# release self-contained for someone who unpacks it off PATH.
- name: Stage the archive
id: stage
run: |
set -euo pipefail
name="pmacs-${{ needs.preflight.outputs.version }}-${{ matrix.target-label }}"
mkdir -p "staging/$name"
cp target/release/pmacs "staging/$name/"
cp target/release/pmacs-gpu "staging/$name/"
cp README.md LICENSE* "staging/$name/" 2>/dev/null || true
chmod +x "staging/$name/pmacs" "staging/$name/pmacs-gpu"
tar -C staging -czf "$name.tar.gz" "$name"
echo "archive=$name.tar.gz" >> "$GITHUB_OUTPUT"
echo "name=$name" >> "$GITHUB_OUTPUT"
# Assert the archive rather than the build command. The build
# could change, a target could be added, a cache could leak -- the
# only thing that decides what users receive is what is inside
# this file.
- name: Assert archive contents
run: |
set -euo pipefail
archive="${{ steps.stage.outputs.archive }}"
name="${{ steps.stage.outputs.name }}"
members=$(tar -tzf "$archive" | sed "s#^$name/##" | grep -v '^$' | sort)
echo "members:"; echo "$members" | sed 's/^/ /'
for forbidden in pmacs-audit pmacs_fake_lsp pmacs_fake_mcp; do
if echo "$members" | grep -qx "$forbidden"; then
echo "::error::$forbidden is present in $archive and must never ship"
exit 1
fi
done
for required in pmacs pmacs-gpu; do
echo "$members" | grep -qx "$required" \
|| { echo "::error::$required missing from $archive"; exit 1; }
done
# Executable bits, read back out of the archive itself.
for required in pmacs pmacs-gpu; do
mode=$(tar -tvzf "$archive" | awk -v f="$name/$required" '$NF==f {print $1}')
case "$mode" in
*x*) : ;;
*) echo "::error::$required is not executable in the archive (mode $mode)"; exit 1 ;;
esac
done
# Run the staged binaries. `--version` is the cheapest end-to-end
# proof that what was built is what will ship, and it is the check
# that would have caught a tag/manifest mismatch reaching this far.
- name: Assert the staged binaries report the tagged version
run: |
set -euo pipefail
expected="${{ needs.preflight.outputs.version }}"
base="${expected%%-*}"
name="${{ steps.stage.outputs.name }}"
got_pmacs=$("staging/$name/pmacs" --version)
got_gpu=$("staging/$name/pmacs-gpu" --version)
echo "pmacs: $got_pmacs"
echo "pmacs-gpu: $got_gpu"
echo "$got_pmacs" | grep -qx "pmacs $base" \
|| { echo "::error::pmacs --version reported '$got_pmacs', expected 'pmacs $base'"; exit 1; }
echo "$got_gpu" | grep -q "^pmacs-gpu $base " \
|| { echo "::error::pmacs-gpu --version reported '$got_gpu', expected 'pmacs-gpu $base ...'"; exit 1; }
# The glibc floor, MACHINE-CHECKED rather than trusted.
#
# Pinning ubuntu-22.04 is what sets the floor, but nothing about a
# pinned runner *proves* the resulting binary honours it, and the
# failure is invisible at build time -- it surfaces as a bare
# `GLIBC_2.xx not found` on a user's machine, with no clue which
# commit caused it. Reading the required symbol versions out of
# the binary turns a runner choice into an assertion, so switching
# this job to `ubuntu-latest` fails HERE instead of shipping.
#
# Linux only: Mach-O has no equivalent versioned-symbol scheme.
- name: Assert the glibc floor
if: runner.os == 'Linux'
run: |
set -euo pipefail
floor="2.35" # Ubuntu 22.04 / Debian 12; see the framing §1.6
name="${{ steps.stage.outputs.name }}"
fail=0
for bin in pmacs pmacs-gpu; do
max=$(objdump -T "staging/$name/$bin" \
| grep -oE 'GLIBC_[0-9]+\.[0-9]+' \
| sed 's/GLIBC_//' | sort -uV | tail -1)
echo "$bin requires at most glibc $max (floor $floor)"
highest=$(printf '%s\n%s\n' "$floor" "$max" | sort -V | tail -1)
if [ "$highest" != "$floor" ]; then
echo "::error::$bin requires glibc $max, above the declared floor $floor." \
"It will not load on Ubuntu 22.04 or Debian 12. Check the runner image."
fail=1
fi
done
[ "$fail" -eq 0 ]
- name: Checksum
run: |
set -euo pipefail
archive="${{ steps.stage.outputs.archive }}"
if command -v sha256sum >/dev/null; then
sha256sum "$archive" > "$archive.sha256"
else
shasum -a 256 "$archive" > "$archive.sha256"
fi
cat "$archive.sha256"
- uses: actions/upload-artifact@v4
with:
name: ${{ steps.stage.outputs.name }}
path: |
${{ steps.stage.outputs.archive }}
${{ steps.stage.outputs.archive }}.sha256
if-no-files-found: error
publish:
name: Publish release
needs: [preflight, build]
runs-on: ubuntu-22.04
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true
# One SHA256SUMS covering every published artifact, alongside the
# per-archive files. Without it a download cannot be verified by
# anyone; with it, verification is one command.
- name: Collect checksums
run: |
set -euo pipefail
cd dist
ls -la
cat ./*.sha256 | sed 's#\./##' | sort -k2 > SHA256SUMS
echo "--- SHA256SUMS ---"; cat SHA256SUMS
sha256sum -c SHA256SUMS
- name: Release notes
run: |
set -euo pipefail
cat > notes.md <<'NOTES'
## Install
Download the archive for your platform, unpack it, and put both
binaries somewhere on your `PATH` — **keep them together**:
`pmacs --gpu` looks for `pmacs-gpu` beside itself first, then
falls back to `PATH`.
Verify a download against `SHA256SUMS`:
```sh
sha256sum -c SHA256SUMS --ignore-missing
```
## Platform support
- **Linux x86_64** — built on Ubuntu 22.04, so the floor is
**glibc ≥ 2.35**. Covers Ubuntu 22.04+ and Debian 12+.
**RHEL 9 (glibc 2.34) is below the floor and not supported
yet.**
- **macOS arm64 (Apple Silicon)** — Intel macOS is not built
yet. The binaries are **unsigned and not notarized**, so
Gatekeeper will quarantine them; you will need to allow them
explicitly.
## Runtime dependencies
Not checked at startup — the editor assumes them:
`/bin/sh`, `stty` and coreutils (PTY raw-mode trampoline, used
by the REPL and terminal), plus `git` and `tar` for package
installation. The Lua VM is statically vendored, so there is no
external Lua dependency.
`pmacs-gpu` additionally needs a working Vulkan (Linux) or
Metal (macOS) adapter. There is no software-rasterizer
fallback in a shipped binary.
## Not in this release
Channels, in-place update, rollback, signing, reproducible
builds, package-manager distribution, and Windows. See
`docs/distribution-stage1-framing.md` §5.
NOTES
echo "notes written"
- name: Publish
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
flags=(--title "pmacs ${{ needs.preflight.outputs.version }}" --notes-file notes.md)
if [ "${{ needs.preflight.outputs.prerelease }}" = "true" ]; then
flags+=(--prerelease)
fi
gh release create "${GITHUB_REF_NAME}" "${flags[@]}" \
dist/*.tar.gz dist/SHA256SUMS

4
Cargo.lock generated
View File

@ -2568,7 +2568,7 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
[[package]]
name = "pmacs"
version = "1.0.0"
version = "1.1.0"
dependencies = [
"arborium-lean",
"codebook-tree-sitter-latex",
@ -2621,7 +2621,7 @@ dependencies = [
[[package]]
name = "pmacs-gpu"
version = "0.0.1"
version = "1.1.0"
dependencies = [
"arboard",
"env_logger",

View File

@ -21,7 +21,7 @@ unicode-width = "0.2"
[package]
name = "pmacs"
default-run = "pmacs"
version = "1.0.0"
version = "1.1.0"
edition = "2024"
rust-version = "1.95"
description = "Parallel Emacs --- a Rust-cored, Lua-scripted editor in the Emacs tradition"

View File

@ -141,6 +141,33 @@ loaded after the builtin runtime so plain assignments override
defaults --- keybindings, `pmacs.lsp.config`, theme overrides, and
package installs all live there.
## Install
Download an archive from the
[releases page](https://github.com/levineuwirth/pmacs/releases), unpack
it, and put both binaries somewhere on your `PATH`.
**Keep `pmacs` and `pmacs-gpu` together.** `pmacs --gpu` looks for
`pmacs-gpu` beside itself first and only then falls back to a `PATH`
lookup, so an unpacked release is self-contained as long as the two
stay in the same directory.
Verify a download:
```sh
sha256sum -c SHA256SUMS --ignore-missing
```
| platform | built on | notes |
|---|---|---|
| Linux x86_64 | Ubuntu 22.04 | requires **glibc ≥ 2.35** — Ubuntu 22.04+, Debian 12+. **RHEL 9 (glibc 2.34) is not supported yet.** |
| macOS arm64 | macOS 15 | Apple Silicon only; Intel is not built yet. Binaries are **unsigned and not notarized**, so Gatekeeper will quarantine them until you allow them explicitly. |
Releases carry binaries only — there is no in-place update, rollback, or
package-manager distribution yet. Build from source for any platform not
listed, and see "Runtime dependencies" below for what the editor assumes
is present.
## Build
Builds on the toolchain pinned in `rust-toolchain.toml` (Rust

View File

@ -1,7 +1,25 @@
# Framing — Distribution Stage 1: binaries on tag
**Revision 2.** Status: **approved with amendments** (revision 1 →
2 records them). Scouted against `githubsucks/main` @ `c5f7501` (#209).
**Revision 3.** Status: **implemented** on branch `distribution-stage1`,
based on `githubsucks/main` @ `4984169` (#210). Approved at revision 2.
**Revision 2 → 3** records two implementation findings, not a new design
round:
- **Layer 2 of the binary exclusion is load-bearing, and this is now
demonstrated rather than argued** (§1.2b). The argument for it was
hypothetical; building the branch produced the exact case it guards
against, on the first try.
- **The glibc floor is machine-checked** (§1.6a), which is stronger than
acceptance 7's original container test and runs on every release
instead of once at RC time.
- **The version bump exposed a real product defect** (§1.3a): the daemon
reported the *protocol* crate's version to every attached frontend
under a field named `pmacs_version`. It was invisible while the two
crates happened to share a number, and Q#D1's decision to diverge them
is what surfaced it. Fixed here, because shipping a release whose
daemon misreports its own version is precisely what this stage
exists to prevent.
`.github/workflows/` contains exactly one workflow and it is test-only.
**There is no release job, no artifact upload, no tags-to-binaries path.**
@ -94,6 +112,60 @@ layer 1 relies on a list nobody re-checks when a new `src/bin/*.rs`
appears. Acceptance 2 asserts the **complete member list**, the
**executable bits**, and the **absence of all three** excluded binaries.
### 1.2b Layer 2 is load-bearing — demonstrated, not argued
Revision 2 justified the second layer with a hypothetical: "a cached
`target/release` can still hold binaries from an earlier build."
**Implementing the branch produced that case immediately.** After
running only
```sh
cargo build --release --bin pmacs --features crdt
cargo build --release -p pmacs-gpu
```
on a tree where earlier work had run `cargo test --release` for the M10
perf gates, `target/release` contained:
```
pmacs pmacs-audit pmacs_fake_lsp pmacs_fake_mcp pmacs-gpu
```
**All three forbidden binaries were present**, left by the earlier test
build, despite this build naming only two targets. `Swatinem/rust-cache`
restores exactly this kind of directory in CI, so the risk is not
theoretical there either.
An implementation that took layer 1 as sufficient and archived
`target/release` would have published a fake language server in the
first release. The three archive assertions are bite-verified: a
smuggled `pmacs_fake_lsp`, a missing `pmacs-gpu`, and a cleared
executable bit are each caught, with the honest archive passing.
### 1.6a The glibc floor is asserted, not trusted
Acceptance 7 originally proposed verifying the floor by running the
binary in containers. **The shipped check is stronger and cheaper**:
read the versioned-symbol requirements straight out of the binary and
fail the build when any exceeds the floor.
```sh
objdump -T <binary> | grep -oE 'GLIBC_[0-9]+\.[0-9]+' | sort -uV | tail -1
```
Why this beats the container test: it runs on **every** release rather
than once at RC time, it needs no network or images, and it fails at the
point of causation. Pinning `ubuntu-22.04` sets the floor but proves
nothing about the artifact — switching the job to `ubuntu-latest` would
otherwise ship binaries that fail to load with a bare `GLIBC_2.39 not
found` on a user's machine, with no clue which commit caused it. With
the assertion, that change fails in CI instead.
Bite-verified in both directions on a glibc 2.44 host, which stands in
for a mis-pinned runner: against a 2.35 floor both binaries are caught;
against a 2.44 floor both pass. Linux only — Mach-O has no equivalent
versioned-symbol scheme.
### 1.2a Why `pmacs` and `pmacs-gpu` must be co-located — corrected
Revision 1 said separating them makes `--gpu` "silently fail." **That is
@ -131,6 +203,36 @@ containing a `pmacs` reporting `1.0.0` and a `pmacs-gpu` reporting
`0.0.1`. **The workflow must refuse rather than publish** (acceptance
4), and acceptance asserts the *binaries'* output, not the manifests.
### 1.3a The bump exposed a defect: the daemon reported the wrong crate's version
**`InstanceIdentity::for_running_process` is defined in
`pmacs-protocol` and expanded `env!("CARGO_PKG_VERSION")` there.**
`env!` expands in the crate being *compiled*, so the field documented as
"Pmacs version string" carried the **protocol crate's** version.
This is not cosmetic. That identity reaches Lua as
`pmacs.instance.identity()` and goes on the wire in `Hello`, so every
attached frontend was told the daemon's version — and after the bump it
would have been told `1.0.0` by a `1.1.0` release.
**Nothing could have detected it before this stage.** Three tests assert
`id.pmacs_version == env!("CARGO_PKG_VERSION")` evaluated in the `pmacs`
crate, which is the correct assertion — but while both crates read
`1.0.0` they compared the same number reached by two different paths and
**could not fail**. Q#D1's decision to hold `pmacs-protocol` at 1.0.0
while moving `pmacs` is what made them discriminating, and all three
failed immediately on the bump.
The fix makes the version a **parameter**, so the `env!` expands in the
caller's crate; all three call sites are in `pmacs` and pass their own.
This is a breaking signature change to a `pub` function in
`pmacs-protocol`, which stays at 1.0.0 per Q#D1 — acceptable because the
crate is a path dependency with no external consumers, and recorded here
rather than silently absorbed.
*A test can be correct and still prove nothing, when the two things it
compares are equal for a reason unrelated to the code under test.*
### 1.4 The existing `v1.0.0` tag is stale and is not re-used
`v1.0.0` is pushed and points at `d3fa632` — the old release mirror's
@ -257,8 +359,10 @@ change the minimum supported macOS without a commit to point at.
5. The tagged commit is an ancestor of `main`.
6. Each artifact is a **CRDT build**, verified by running the shipped
binary rather than trusting the flag (§1.5).
7. The Linux binary **runs on Ubuntu 22.04 and Debian 12** — verified in
containers — and its glibc floor is stated in the release notes.
7. The Linux binary honours the **glibc ≥ 2.35** floor, asserted from
the binary's own versioned symbols on every release (§1.6a) rather
than by a one-off container run, and the floor is stated in the
release notes and README.
8. `SHA256SUMS` covers every published artifact and verifies against the
downloads.
9. Release notes carry the runtime dependencies (§1.7), the glibc floor,

View File

@ -1,6 +1,6 @@
[package]
name = "pmacs-gpu"
version = "0.0.1"
version = "1.1.0"
edition = "2024"
rust-version = "1.95"
description = "GPU/GUI frontend for pmacs — attach, editing, syntax/diagnostics/minimap, mouse + context menu, clipboard, search, minibuffer. See docs/pmacs-gpu-design.md."

View File

@ -1885,13 +1885,30 @@ impl InstanceIdentity {
/// call site, so calling twice on different days surfaces different
/// uptimes from the same anchor.
///
/// The version comes from `CARGO_PKG_VERSION` and the build hash
/// from the optional `PMACS_GIT_HASH` environment variable populated
/// by the build script.
/// `pmacs_version` is supplied BY THE CALLER, and must be
/// `env!("CARGO_PKG_VERSION")` evaluated in the `pmacs` crate.
///
/// It is a parameter rather than an `env!` here because `env!`
/// expands in the crate being COMPILED: reading it inside
/// `pmacs-protocol` yields the *protocol* crate's version, which
/// this field is not. The two crates held the same number until the
/// 1.1.0 release bump moved `pmacs` and left `pmacs-protocol` at
/// 1.0.0 — at which point the daemon began reporting the protocol
/// crate's version to every attached frontend, under a field named
/// `pmacs_version`. Nothing detected it earlier because the values
/// had always agreed by coincidence.
///
/// The build hash still comes from the optional `PMACS_GIT_HASH`
/// environment variable populated by the build script; that one is
/// genuinely crate-independent.
#[must_use]
pub fn for_running_process(instance_name: Option<String>, started: std::time::Instant) -> Self {
pub fn for_running_process(
pmacs_version: &str,
instance_name: Option<String>,
started: std::time::Instant,
) -> Self {
Self {
pmacs_version: env!("CARGO_PKG_VERSION").into(),
pmacs_version: pmacs_version.into(),
build_hash: option_env!("PMACS_GIT_HASH").map(String::from),
instance_name,
uptime_secs: started.elapsed().as_secs(),

View File

@ -2393,6 +2393,7 @@ mod tests {
protocol_version: PROTOCOL_VERSION + 999,
assigned_frontend_id: FrontendId::LOCAL,
instance_identity: InstanceIdentity::for_running_process(
env!("CARGO_PKG_VERSION"),
None,
std::time::Instant::now(),
),

View File

@ -422,7 +422,11 @@ impl DaemonState {
}
fn build_identity(&self) -> InstanceIdentity {
InstanceIdentity::for_running_process(self.instance_name.clone(), self.started)
InstanceIdentity::for_running_process(
env!("CARGO_PKG_VERSION"),
self.instance_name.clone(),
self.started,
)
}
/// `--socket NAME` value the daemon was launched with, or `None`

View File

@ -395,7 +395,11 @@ impl LocalInstanceInfo {
#[must_use]
pub fn build_identity(&self) -> InstanceIdentity {
let data = self.0.borrow();
InstanceIdentity::for_running_process(data.name.clone(), data.started)
InstanceIdentity::for_running_process(
env!("CARGO_PKG_VERSION"),
data.name.clone(),
data.started,
)
}
}