From 239b2458b8dcb33e830d6f33d55d34c4fcb99b5a Mon Sep 17 00:00:00 2001 From: "David M. Anderson" Date: Thu, 17 Sep 2026 22:15:30 -0500 Subject: [PATCH 1/3] One certificate in two keychains is one identity, not two `find_identity` counted lines from `security find-identity`, which searches every keychain in the search list. A Mac set up to sign over ssh has a signing keychain holding a copy of what the login keychain already had, which `mac-signing-keychain.sh` puts there deliberately, so every identity is reported at least twice. Here it was four times, and the check read that as four Apple Distribution certificates and refused to sign anything. It now counts distinct SHA-1s. The case the check was written for is untouched: two genuinely different certificates sharing a display name, an expiring one beside its replacement, still refuses, and that was tried both ways before the change went in. Measured: the three Store packages build and sign, each carrying `Authority=Apple Distribution: Excelano LLC (9K6W5PMFYP)`, and `altool --validate-app` returns VERIFY SUCCEEDED for all three. Co-Authored-By: Claude Opus 5 (1M context) --- packaging/macos/build-app.sh | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packaging/macos/build-app.sh b/packaging/macos/build-app.sh index 81aa161..70b32d6 100755 --- a/packaging/macos/build-app.sh +++ b/packaging/macos/build-app.sh @@ -517,16 +517,27 @@ build_store() { # one keychain is an ordinary state, an expiring one beside its replacement, # and picking whichever `grep` found first is how a package gets signed with # the wrong one. + # + # One certificate can be reported several times over. `find-identity` + # searches every keychain in the search list, and the signing keychain + # `mac-signing-keychain.sh` makes holds a copy of what the login keychain + # already had, so a Mac set up to sign over ssh lists each identity at least + # twice. That is one identity seen twice and not two identities, and the + # SHA-1 each is listed under is what tells them apart. Counting lines + # refused every machine that had a signing keychain at all. find_identity() { matches=$(security find-identity -v 2>/dev/null | - grep "$1: .*(${store_team})" | sed 's/.*"\(.*\)"/\1/') + grep "$1: .*(${store_team})" | + sed 's/^ *[0-9]*) *\([0-9A-F]*\) *"\(.*\)"$/\1 \2/' | + sort -u) count=$(printf '%s' "$matches" | grep -c . || true) [ "$count" = 1 ] || { echo "build-app.sh: expected one \"$1\" identity for team ${store_team}, found ${count}" >&2 [ "$count" = 0 ] || echo "$matches" | sed 's/^/ /' >&2 return 1 } - printf '%s' "$matches" + # The hash was for telling them apart; what signs is the name. + printf '%s' "${matches#* }" } app_identity=$(find_identity "Apple Distribution") || exit 1 # Apple's portal calls this Mac Installer Distribution; the certificate calls From 1b0d5be59d52dd09eff1bd90649624b4ba16436d Mon Sep 17 00:00:00 2001 From: "David M. Anderson" Date: Thu, 17 Sep 2026 22:16:10 -0500 Subject: [PATCH 2/3] A capture is checked against the sizes App Store Connect takes The window is sized in points and `screencapture` writes pixels, so a display with a backing scale of 2 hands back twice what was asked for. Both are right: Connect takes 1280x800, 1440x900, 2560x1600 and 2880x1800, and the larger pair is the smaller pair doubled. Comparing the file against the number asked for would call a correct Retina capture wrong and refuse it. Not reached on this Mac, whose display is 1x and whose captures come back at the 1440x900 the window was sized to. It is the refusal a Retina machine would have met on its first run, and the message named the sizes without saying what they are, which is the other half of why it would have been puzzling. The second condition keeps what the first one gave up: an accepted size that is not this window at either scale is some other window, and still refuses. Co-Authored-By: Claude Opus 5 (1M context) --- packaging/macos/screenshot.sh | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packaging/macos/screenshot.sh b/packaging/macos/screenshot.sh index d827cc1..fbdfce3 100755 --- a/packaging/macos/screenshot.sh +++ b/packaging/macos/screenshot.sh @@ -387,8 +387,21 @@ screencapture -x -o -l "$id" "$out" got_w=$(sips -g pixelWidth "$out" | sed -n 's/.*pixelWidth: *//p') got_h=$(sips -g pixelHeight "$out" | sed -n 's/.*pixelHeight: *//p') -if [ "$got_w" != "$width" ] || [ "$got_h" != "$height" ]; then - refuse "asked for ${width}x${height} and got ${got_w}x${got_h} — App Store Connect refuses anything but its own sizes" +# The window is sized in points and the capture is written in pixels, so a +# display with a backing scale of 2 returns twice what was asked for. Both are +# right: App Store Connect takes 1280x800, 1440x900, 2560x1600 and 2880x1800, +# and the larger pair is the smaller pair doubled. Comparing the file against +# the number asked for called a correct Retina capture wrong and refused it. +case "${got_w}x${got_h}" in + 1280x800|1440x900|2560x1600|2880x1800) ;; + *) + refuse "got ${got_w}x${got_h}, and App Store Connect takes 1280x800, 1440x900, 2560x1600 or 2880x1800 and nothing else" + ;; +esac +# Still the window that was asked for, at one scale or the other. A capture +# that is an accepted size but not this one is some other window. +if [ "$got_w" != "$width" ] && [ "$got_w" != "$((width * 2))" ]; then + refuse "asked for ${width}x${height} and got ${got_w}x${got_h}, which is neither that nor that at a backing scale of 2" fi echo "${out}: ${got_w}x${got_h}, window ${id} of ${product}" From 360f995081cbaa58c56027bb17a739221f941cf5 Mon Sep 17 00:00:00 2001 From: "David M. Anderson" Date: Thu, 17 Sep 2026 22:16:20 -0500 Subject: [PATCH 3/3] The macOS set is taken in a language, and a recipe layer takes it `screenshot.sh` gains `--lang`, passed to the application through `open --env` as POTEXT_LANG. It cannot be exported by the calling shell: `open` hands the process to launchd, and launchd does not pass this shell environment on. That is the shape flyleaf's driver already uses, and this is the last of the four window lanes to get it. It also gains `--settle`. A deck whose slides carry pictures is still decoding them when a document of a few pages has settled, and `shots.ps1` gives xodp eight seconds for the same reason. `shots.sh` is the half that is odox's: which application, which document, and which locale directory. It is the counterpart of `packaging/windows/shots.ps1` and carries the same table in this platform's spellings, writing `dist/screenshots//` - the layout `ship --shots` reads and the Windows lane already writes. It photographs a development-signed bundle, because a Store build cannot be launched on the machine that made it. Measured: six frames, three applications in each of en-US and de-DE, 1440x900, with German chrome in the German set - Datei, Ansicht, Gliederung, Folien, and 100 % with the German space. Co-Authored-By: Claude Opus 5 (1M context) --- packaging/macos/screenshot.sh | 27 ++++++++- packaging/macos/shots.sh | 110 ++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 2 deletions(-) create mode 100755 packaging/macos/shots.sh diff --git a/packaging/macos/screenshot.sh b/packaging/macos/screenshot.sh index fbdfce3..c089b06 100755 --- a/packaging/macos/screenshot.sh +++ b/packaging/macos/screenshot.sh @@ -14,11 +14,24 @@ # --out shots/xods-02-sheet.png # ./packaging/macos/screenshot.sh xodp --app dist/'Odox Deck'.app \ # --document corpus/libreoffice/focus.odp --out shots/xodp-01.png +# ./packaging/macos/screenshot.sh xodt --lang de --out shots/de-DE/xodt-01.png # # The bundle defaults to `dist/.app`, which is where `build-app.sh` # puts it, and the document to the corpus file `packaging/store-listing.md` # names for that application. Only `--out` has no sensible default. # +# `--lang` photographs the window in that language. A listing in two languages +# wants a set in each, and a German listing showing an English window is the +# inaccurate metadata guideline 2.3.3 is about. It is passed through +# `open --env` as POTEXT_LANG, which every application in this fleet reads +# before it asks the platform. It has to go through `--env` and cannot be +# exported here: `open` hands the process to launchd, and launchd does not pass +# this shell's environment on. +# +# `--settle` waits longer before the shutter. A deck whose slides carry pictures +# is still decoding them when a document of a few pages has settled, which is +# why `shots.sh` gives xodp more than the other two. +# # FOUR ACTIONS, IN THE ORDER GIVEN # # `--click X,Y` presses a control. `--double X,Y` presses it twice inside the @@ -94,6 +107,10 @@ name="" bundle="" document="" out="" +lang="" +# Seconds between the window being sized and the capture. Five is enough for a +# text document and not for a deck; `--settle` is how a caller says so. +settle=5 # 1440x900 is one of the four sizes App Store Connect accepts for macOS, and the # largest reachable without a Retina display. The other two — 2560x1600 and # 2880x1800 — need a backing scale of 2, which is why they are not the default. @@ -142,6 +159,8 @@ while [ $# -gt 0 ]; do --double) echo "double ${2:?--double needs X,Y}" >> "$actions"; shift 2 ;; --type) echo "type ${2?--type needs text}" >> "$actions"; shift 2 ;; --key) echo "key ${2:?--key needs a name}" >> "$actions"; shift 2 ;; + --lang) lang="${2:?--lang needs a language tag}"; shift 2 ;; + --settle) settle="${2:?--settle needs seconds}"; shift 2 ;; --width) width="${2:?}"; shift 2 ;; --height) height="${2:?}"; shift 2 ;; --x) x="${2:?}"; shift 2 ;; @@ -336,8 +355,12 @@ swiftc -O -o "$helper" "$source" || refuse "the helper did not compile" pkill -f "${bundle}/Contents/MacOS/" 2>/dev/null || true sleep 1 -open -a "$bundle" "$document" -sleep 5 +if [ -n "$lang" ]; then + open -a "$bundle" --env "POTEXT_LANG=${lang}" "$document" +else + open -a "$bundle" "$document" +fi +sleep "$settle" # Found once, by the path of the executable inside this bundle, and used by id # everywhere below. The header says why not by name. diff --git a/packaging/macos/shots.sh b/packaging/macos/shots.sh new file mode 100755 index 0000000..64caa5c --- /dev/null +++ b/packaging/macos/shots.sh @@ -0,0 +1,110 @@ +#!/bin/sh +# The Mac App Store screenshots, as recipes rather than as prose. +# +# `screenshot.sh` beside this is the driver and knows nothing about Odox. This +# file is the part that is Odox's — which application, which document, and what +# has to happen in the window before the shutter. It is the counterpart of +# `packaging/windows/shots.ps1`, and the two carry the same table in their own +# platform's spellings. +# +# MACOSX_DEPLOYMENT_TARGET=11.0 cargo build --release +# ./packaging/macos/build-app.sh --all --sign "Apple Development: ..." +# ./packaging/macos/shots.sh # the English set +# ./packaging/macos/shots.sh --lang de # and the German one +# ./packaging/macos/shots.sh --only xodt # one application's frames +# +# Run it in Terminal at the console. Sizing another application's window goes +# through System Events, which is gated on Accessibility permission, and an ssh +# session cannot be granted it. +# +# WHAT IS PHOTOGRAPHED, WHICH IS NOT THE THING THAT SHIPS +# +# A signed development bundle built from the commit being released. The Store +# build cannot be launched on the machine that made it — the kernel refuses its +# entitlements without a profile covering this Mac, and a Store profile covers +# none — so no screenshot can ever be of the exact artefact that gets uploaded. +# Build both from one commit and say which in `packaging/store-listing.md`. +# +# Something is open in every shot: a screenshot of an empty window is what +# guideline 2.3.3 sends back. +# +# Author: David M. Anderson +# Built with AI assistance (Claude, Anthropic) +set -eu + +here=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +root=$(CDPATH= cd -- "${here}/../.." && pwd) + +only="" +lang="" +bundles="${root}/dist" +outdir="" + +usage() { + sed -n '2,29p' "$0" | sed 's/^# \{0,1\}//' + exit "${1:-0}" +} + +refuse() { echo "shots.sh: $1" >&2; exit 1; } + +while [ $# -gt 0 ]; do + case "$1" in + --only) only="${2:?--only needs xodt, xods or xodp}"; shift 2 ;; + --lang) lang="${2:?--lang needs a language tag}"; shift 2 ;; + --bundles) bundles="${2:?--bundles needs a directory}"; shift 2 ;; + --outdir) outdir="${2:?--outdir needs a directory}"; shift 2 ;; + -h|--help) usage 0 ;; + *) echo "shots.sh: unknown argument $1" >&2; usage 2 ;; + esac +done + +# The language is a tag and the directory it lands in is a locale, and they are +# not the same string. The Windows lane maps them the same way, in +# `take-shots.ps1`, because a frame's directory is half the name the Store files +# it under and the two lanes have to agree. +case "$lang" in + "") locale="" ;; + en*) locale="en-US" ;; + de*) locale="de-DE" ;; + *) refuse "no locale is known for $lang" ;; +esac + +# Where the shots land. Not committed: dist is where every built artefact goes. +[ -n "$outdir" ] || outdir="${root}/dist/screenshots" +[ -z "$locale" ] || outdir="${outdir}/${locale}" + +# The three strings each application needs here. `build-app.sh` holds the full +# table and `packaging/windows/shots.ps1` holds the same one in that platform's +# spellings; only these columns are wanted. +# +# A deck whose slides carry pictures is still decoding them when a document of a +# few pages has settled, so xodp waits longer than the driver's default. The +# Windows lane gives it the same eight seconds. +for app in xodt xods xodp; do + [ -z "$only" ] || [ "$only" = "$app" ] || continue + case "$app" in + xodt) product="Odox Text"; document="corpus/libreoffice/text.odt"; settle=5 ;; + xods) product="Odox Grid"; document="corpus/libreoffice/calc.ods"; settle=5 ;; + xodp) product="Odox Deck"; document="corpus/libreoffice/growing-liberty.odp"; settle=8 ;; + esac + bundle="${bundles}/${product}.app" + [ -d "$bundle" ] || refuse "no bundle at ${bundle} — build one with 'packaging/macos/build-app.sh ${app} --sign ...'" + + mkdir -p "$outdir" + out="${outdir}/${app}-01-document.png" + + # No actions yet. These three read a document and do not edit one, so the + # document on screen is what they do; a frame wanting a pane opened or a + # slide selected takes a coordinate, and a coordinate is read off a frame + # of the same size rather than guessed. + echo "==> ${app}${lang:+ (${lang})}" + "${here}/screenshot.sh" "$app" \ + --app "$bundle" \ + --document "${root}/${document}" \ + --settle "$settle" \ + ${lang:+--lang "$lang"} \ + --out "$out" +done + +echo +find "$outdir" -name '*.png' | sort