2 Commits

Author SHA1 Message Date
68a50d10b1 Record the desktop work: the tray, macOS, and the release
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
2026-09-30 15:01:55 +05:30
8137480877 release.sh can build the macOS package too
Cheap for a reason worth stating: the Windows package is already a SOURCE
install - a pure-Python wheel plus a setup tool that builds a venv on the
target machine, because PyInstaller cannot cross-compile. macOS needs
nothing different. Same wheel, same setup tool, a natively built .app in
place of the .exe. No frozen engine, no 200 MB, no second packaging story.

behavision-setup already handled both platforms (Scripts vs bin, the
tasklist check guarded) and cross-compiled for darwin without a change.
The one thing that did not was its advice when Python is missing: it told
everyone to tick 'Add python.exe to PATH' on a Windows installer page.
Software that does not know which machine it is running on is software
somebody stops trusting for the rest of the session.

MAC=1 opts in, so the ordinary Windows release is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
2026-09-30 14:50:45 +05:30
3 changed files with 136 additions and 6 deletions

View File

@@ -2568,6 +2568,93 @@ raw string that is two literal characters and Postgres requires exactly one —
it would have failed the **whole** customer search at runtime, on a query no
in-memory test executes.
## The desktop app on two platforms, and three bugs found by launching it
All three were reported or found by *starting the app the way a person
starts one*, and all three had survived every test.
### "Open dashboard" in the tray did nothing reliable
`runtime.Show` was wrong three times over and the first is why it failed
rather than merely misbehaved. Wails implements `Show()` as a bare
`mainWindow.Show()` while `WindowShow()` wraps the identical work in
`runtime.LockOSThread`. Win32 window operations must run on the thread owning
the window's message pump, and the tray handler runs on the **systray's**
goroutine, which never is.
Two more, each sufficient alone: showing is not un-minimising (hidden and
minimised are different states), and Windows refuses the foreground to a
process that does not already hold it — so the window returned *behind*
whatever was being looked at. A tray click is by definition a moment when the
app is not in front, so that is every time, not an edge case. The
always-on-top flip is the ordinary way to ask, and it is why this runs in a
goroutine: a menu loop that sleeps is a tray that ignores the next click.
`OnSecondInstanceLaunch` had the same shape and is hit far more often —
double-clicking the desktop icon while the app is already running.
### The engine inherited whatever directory launched the app
Nothing ever set `cmd.Dir`, so the child took the parent's — and an app
started by double-clicking its bundle is handed `/`. On macOS the symptom was
`python: No module named behavision` forever, because the dev engine runs as
`-m behavision`, which resolves against the working directory.
**The same app launched from a terminal inside the repo worked perfectly**,
which is the shape of a bug that survives every test a developer runs.
`Config.EngineDir` (empty = install root) is set by both launchers, which had
identical code and the identical omission.
### macOS is a supported DEVELOPER target, not a product
Indian retail counters are Windows. A Mac product means an Apple Developer
account, notarisation, a second installer, and DPAPI having no macOS
equivalent — a permanent second platform for customers who do not have Macs.
What it *is* worth is demoing on the machine this is written on.
It cost one missing framework and then two crashes:
```
link Undefined symbols: _OBJC_CLASS_$_UTType
Wails' darwin frontend references it and does not link
UniformTypeIdentifiers. Fails at the LINK step after compiling
everything, so it reads like a broken toolchain.
systray.Run SIGTRAP in cgo — nativeLoop takes the macOS main
run loop and Wails already has it
RunWithExternalLoop "NSWindow should only be instantiated on the main
thread!" — still builds AppKit objects, and
OnStartup is not the main thread
```
So **there is no tray on macOS**, and the consequence is handled rather than
left: with no tray there is no way back from a hidden window and no way to
quit, so on macOS closing the window quits and stops the engine. Same rule the
tray's Quit follows — never leave it watching with no visible control.
**The window could not be maximised**, and that was an omission with a precise
consequence. Wails computes `zoomable` *inside* `if frontendOptions.Mac !=
nil`; the variable defaults to 0, and the native side then does
`if (!zoomable && resizable) [zoomButton setEnabled: NO]`. There was a
`Windows` options block and no `Mac` one — so the platform that was configured
behaved and the platform that was not looked broken.
### A macOS release costs nothing extra, and the reason is worth keeping
`release.sh` has never used PyInstaller. The Windows package is a **source
install**: a pure-Python wheel plus `behavision-setup`, which builds a venv on
the target machine, done that way because PyInstaller cannot cross-compile.
macOS therefore needs nothing new — same wheel, same setup tool, a natively
built `.app` instead of the `.exe`. `MAC=1 ./release.sh` opts in.
Not notarised, and that is stated in the notes rather than discovered: macOS
*blocks* an unsigned download rather than warning like SmartScreen, so a first
launch needs right-click → Open.
Verified by extracting the published zip to a clean directory: signature
intact through the round trip, the app runs, and it reports *"engine not
installed yet; run behavision-setup, then Start"* — the correct fresh-machine
state rather than a crash.
## Setting up on a new machine
1. Copy the `Behavision` folder **including `.env`** (gitignored, holds

View File

@@ -266,10 +266,19 @@ func findPython() (string, string, error) {
}
}
msg := "no Python 3.10 or newer was found on this PC.\n\n" +
" Install it from https://www.python.org/downloads/windows/\n" +
" and tick \"Add python.exe to PATH\" on the first screen,\n" +
" then run this again."
// The advice has to match the machine. Telling a Mac user to tick "Add
// python.exe to PATH" on a Windows installer page reads as software that
// does not know where it is running, which is exactly the moment somebody
// stops trusting the rest of what it says.
msg := "no Python 3.10 or newer was found on this computer.\n\n"
if runtime.GOOS == "windows" {
msg += " Install it from https://www.python.org/downloads/windows/\n" +
" and tick \"Add python.exe to PATH\" on the first screen,\n" +
" then run this again."
} else {
msg += " Install it with `brew install python@3.12`, or from\n" +
" https://www.python.org/downloads/macos/, then run this again."
}
if len(tried) > 0 {
msg += "\n\n Found, but too old: " + strings.Join(tried, ", ")
}

View File

@@ -1,8 +1,9 @@
#!/usr/bin/env bash
# Build the Windows shop-PC package and publish it as a Gitea release.
#
# ./release.sh v0.4.2 build dist/Behavision-v0.4.2-windows-x64.zip and publish
# ./release.sh v0.4.2 build the Windows zip and publish
# PUBLISH=0 ./release.sh v0.4.2 build only
# MAC=1 ./release.sh v0.4.2 also build and attach the macOS zip
# DEMO_PACK=demo-cameras.enc ./release.sh v0.4.4-demo
# a demo build: the sealed bundle from
# behavision-demo-pack ships in engine-src,
@@ -22,6 +23,8 @@ TAG=${1:?usage: release.sh vX.Y.Z}
REPO_API=https://gitapp.workolik.com/api/v1/repos/Loyaly/Behavision
STAGE=dist/Behavision
ZIP="dist/Behavision-$TAG-windows-x64.zip"
MACZIP="dist/Behavision-$TAG-macos-arm64.zip"
MACSTAGE=dist/Behavision-mac
step() { printf '\n\033[1m%s\033[0m\n' "$*"; }
case "$(git describe --tags --always --dirty)" in *-dirty) echo "refusing to release uncommitted changes" >&2; exit 1;; esac
@@ -62,9 +65,39 @@ step "4. Package"
rm -f "$ZIP" && (cd dist && zip -qr "$(basename "$ZIP")" Behavision) && ls -la "$ZIP" | awk '{print " " $5 " bytes " $9}'
unzip -l "$ZIP" | grep -E "Behavision\.exe|agent\.exe|setup\.exe|\.whl|INSTALL" | awk '{print " " $4}'
# --- macOS, same SOURCE-install shape as Windows -------------------------
#
# Worth stating because it is the reason this is cheap: the Windows package
# already ships a pure-Python WHEEL and builds a venv on the target machine,
# because PyInstaller cannot cross-compile. macOS needs nothing different -
# the same wheel, the same setup tool, a natively built .app instead of an
# .exe. No frozen engine, no 200 MB, no second packaging story.
#
# Two things it is NOT, and the release notes should say so:
# * not notarised. macOS blocks an unsigned download outright rather than
# warning like SmartScreen, so the first launch needs right-click > Open.
# Notarising needs an Apple Developer account.
# * arm64 only. Every Mac worth demoing on since 2020, and building a
# universal binary doubles the size for machines nobody here has.
if [ "${MAC:-0}" = "1" ]; then
step "5. macOS package"
command -v wails >/dev/null || { echo "wails CLI not found; go install github.com/wailsapp/wails/v2/cmd/wails@latest" >&2; exit 1; }
rm -rf "$MACSTAGE" && mkdir -p "$MACSTAGE/engine-src"
(cd desktop && wails build -platform darwin/arm64 -tags "desktop,production" -skipbindings >/dev/null)
cp -R desktop/build/bin/Behavision.app "$MACSTAGE/"
(cd agent && CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -trimpath \
-ldflags "-s -w -X main.version=$TAG" -o "../$MACSTAGE/behavision-agent" . \
&& CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -trimpath \
-ldflags "-s -w -X main.version=$TAG" -o "../$MACSTAGE/behavision-setup" ./cmd/behavision-setup)
# The identical engine payload the Windows package carries.
cp -R "$STAGE/engine-src/." "$MACSTAGE/engine-src/"
rm -f "$MACZIP" && (cd dist && zip -qry "$(basename "$MACZIP")" Behavision-mac)
ls -la "$MACZIP" | awk '{print " " $5 " bytes " $9}'
fi
[ "${PUBLISH:-1}" = "1" ] || { echo "built, not published"; exit 0; }
step "5. Tag and publish"
step "6. Tag and publish"
git rev-parse -q --verify "refs/tags/$TAG" >/dev/null || git tag -a "$TAG" -m "$TAG"
git push -q origin "$TAG"
# The notes come from a file so they are reviewed, not typed into a shell.
@@ -77,4 +110,5 @@ BODY=$(python3 -c 'import json,sys;print(json.dumps({"tag_name":sys.argv[1],"nam
REL=$(curl -sS -u "$USER:$PASS" -H 'content-type: application/json' -d "$BODY" "$REPO_API/releases")
ID=$(printf '%s' "$REL" | python3 -c 'import json,sys;print(json.load(sys.stdin)["id"])')
curl -sS -u "$USER:$PASS" -F "attachment=@$ZIP" "$REPO_API/releases/$ID/assets?name=$(basename "$ZIP")" >/dev/null
[ "${MAC:-0}" = "1" ] && curl -sS -u "$USER:$PASS" -F "attachment=@$MACZIP" "$REPO_API/releases/$ID/assets?name=$(basename "$MACZIP")" >/dev/null
echo " published: https://gitapp.workolik.com/Loyaly/Behavision/releases/tag/$TAG"