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 diff --git a/packaging/macos/screenshot.sh b/packaging/macos/screenshot.sh index d827cc1..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. @@ -387,8 +410,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}" 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