Files
RP412/stamp-version.ps1
T
CydandClaude Opus 5 eb17220dd5 Test builds go stale after a fortnight
A tester still racing a two-week-old binary reports things that were
fixed a week ago, and the afternoon spent chasing them is gone. An
expired build now says so and stops: a dialog naming its version and
expiry date, pointing at the releases page, and an exit before anything
else runs. The log carries the same line, so a report from an expired
build identifies itself.

$expireDays at the top of stamp-version.ps1 is the shelf life, sitting
next to the product line it belongs with. It counts from the day a build
was MADE rather than the day the code was written - rebuilding an old
commit to chase something should hand back a usable binary, not one born
stale.

SET IT TO 0 FOR A REAL RELEASE. A shipped build that expires is a
catastrophe, and that one line decides it. It is called out in the
script, in the generated header and in BUILD.md, because it is the kind
of thing that gets noticed exactly once, too late.

The date is what makes rpl4build.h differ from one day to the next, so
the first build of each day recompiles RPL4.CPP and the rest do not.

This is a nudge, not a lock. The date comes from the machine's own clock
and anyone determined can wind it back; the point is to stop an honest
tester wasting a day, not to stop anybody at all. RP412NOEXPIRY=1 waives
it for us and logs that it did, so a waived build is never mistaken for
a current one. It is deliberately absent from environ.ini - a bypass
every tester can see is a bypass every tester will use, and then it never
goes stale for the one person it was meant to stop.

Verified all four ways by backdating the shelf life rather than touching
the clock, which is what a negative $expireDays is for: a fresh build
runs untouched; an expired one raises the dialog, exits 1, and logs
"Build expired on 4 August 2026 - refusing to run"; the same expired
build with RP412NOEXPIRY=1 runs and logs the waiver; and a build with two
days left runs and logs two days left.

Two things that only showed up by running it. Negative days first meant
"never" rather than "already expired", so the refusal path went untested
on the first pass - only 0 means never now. And the days-left count was
anchored at midday, reporting one day fewer than the build had; it is
anchored at the end of the expiry day, which is the rule the check
actually enforces.

The packaged README tells testers the build expires, where to get the
next one, and that unzipping it over the folder keeps their four files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 16:35:22 -05:00

149 lines
5.7 KiB
PowerShell

# ============================================================================
# stamp-version.ps1 - write RP_L4\rpl4build.h from the repository's own state
# ============================================================================
#
# The patch number IS the commit count, so a build names the commit it came
# from and there is never a question about which changes are in a binary
# somebody is holding. Run as RP_L4's pre-build step; also readable by
# pack-dist.ps1, so the package and the exe cannot disagree.
#
# A hardcoded version could not do this: the commit that records "4.12.96"
# is itself commit 96, so the file is stale the moment it is committed.
#
# Usage: powershell -ExecutionPolicy Bypass -File stamp-version.ps1
#
$ErrorActionPreference = 'Stop'
# The product line. Bump this by hand when the line moves; the patch
# number after it looks after itself.
$line = '4.12'
# How many days a build stays good for. Test builds go stale so nobody is
# still racing a fortnight-old binary and reporting things that were fixed
# a week ago - the expired build says so and stops, rather than quietly
# wasting everyone's afternoon.
#
# SET THIS TO 0 FOR A REAL RELEASE. A shipped build that expires is a
# catastrophe, and this is the one line that decides it.
$expireDays = 14
$root = Split-Path -Parent $MyInvocation.MyCommand.Path
$header = Join-Path $root 'RP_L4\rpl4build.h'
$count = 0
$commit = 'nogit'
$dirty = 0
#
# Every git call goes through cmd so that stderr never reaches PowerShell's
# error stream. Windows PowerShell turns a native command's stderr into
# ErrorRecords, and with $ErrorActionPreference = 'Stop' git's routine
# "LF will be replaced by CRLF" warning is enough to throw - which silently
# skipped the dirty check and stamped every modified build as clean.
#
try {
Push-Location $root
$c = cmd /c "git rev-list --count HEAD 2>NUL"
if ($LASTEXITCODE -eq 0 -and $c) {
$count = [int]$c
$commit = (cmd /c "git rev-parse --short HEAD 2>NUL").Trim()
#
# Tracked modifications only. An untracked scratch file in the tree
# is not in the binary, and marking every build dirty for one would
# make the marker mean nothing.
#
cmd /c "git diff --quiet HEAD -- 2>NUL"
if ($LASTEXITCODE -ne 0) { $dirty = 1 }
}
} catch {
# no git, or not a repository: fall through to the placeholder below
} finally {
Pop-Location
}
if ($count -eq 0) {
# Built outside the repository (a source drop, say). Say so plainly
# rather than inventing a number that would sort against real ones.
$version = "$line.x"
$long = "$line.x (no repository)"
} else {
$version = "$line.$count"
$long = "$version ($commit$(if ($dirty) { '+' } else { '' }))"
}
#
# Expiry, from the day it was BUILT rather than the day the code was
# written: rebuilding an old commit to chase something should give a
# usable binary, not one that was born stale.
#
# The date is what makes this header change from one day to the next, so
# the first build of each day recompiles RPL4.CPP and the rest of that
# day's builds do not. One file, a couple of seconds.
#
# Only 0 means "never". A NEGATIVE count backdates the expiry, which is how
# the refusal gets tested without touching the machine's clock.
if ($expireDays -ne 0) {
$expiry = (Get-Date).Date.AddDays($expireDays)
$expires = 1
$expiryY = $expiry.Year
$expiryM = $expiry.Month
$expiryD = $expiry.Day
$expiryTxt = $expiry.ToString('d MMMM yyyy', [Globalization.CultureInfo]::InvariantCulture)
} else {
$expires = 0
$expiryY = 0
$expiryM = 0
$expiryD = 0
$expiryTxt = 'never'
}
$content = @"
//===========================================================================//
// File: rpl4build.h GENERATED - do not edit, do not commit //
//---------------------------------------------------------------------------//
// Written by stamp-version.ps1 as RP_L4's pre-build step. The patch number //
// is the repository's commit count and the hash beside it names the exact //
// commit, so a running build always says where it came from. A trailing '+' //
// means the tree had uncommitted changes to tracked files when it was built. //
// //
// EXPIRES is the test-build shelf life - see \$expireDays in the script. A //
// release must be built with it at 0. //
//===========================================================================//
#pragma once
#define RP412_BUILD_COUNT $count
#define RP412_BUILD_COMMIT "$commit"
#define RP412_BUILD_DIRTY $dirty
#define RP412_VERSION "$version"
#define RP412_VERSION_LONG "$long"
#define RP412_EXPIRES $expires
#define RP412_EXPIRY_YEAR $expiryY
#define RP412_EXPIRY_MONTH $expiryM
#define RP412_EXPIRY_DAY $expiryD
#define RP412_EXPIRY_TEXT "$expiryTxt"
"@
#
# Only rewrite when something actually changed: an unconditional write
# would touch the header on every build and drag RPL4.CPP through a
# recompile each time.
#
$existing = if (Test-Path $header) { [IO.File]::ReadAllText($header) } else { '' }
$note = if ($expires) { "expires $expiryTxt" } else { "no expiry" }
if ($existing -ne $content) {
[IO.File]::WriteAllText($header, $content, (New-Object System.Text.ASCIIEncoding))
Write-Host "stamp-version: $long, $note"
} else {
Write-Host "stamp-version: $long, $note (unchanged)"
}
#
# Explicitly: this runs as a pre-build step, and the last thing above it is
# "git diff --quiet", which exits 1 to mean "there are changes". Letting that
# escape would fail the build on every modified tree - the exact case a
# developer builds in.
#
exit 0