Compare commits
2 Commits
f5eb2bb124
...
68a50d10b1
| Author | SHA1 | Date | |
|---|---|---|---|
| 68a50d10b1 | |||
| 8137480877 |
87
CLAUDE.md
87
CLAUDE.md
@@ -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
|
it would have failed the **whole** customer search at runtime, on a query no
|
||||||
in-memory test executes.
|
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
|
## Setting up on a new machine
|
||||||
|
|
||||||
1. Copy the `Behavision` folder **including `.env`** (gitignored, holds
|
1. Copy the `Behavision` folder **including `.env`** (gitignored, holds
|
||||||
|
|||||||
@@ -266,10 +266,19 @@ func findPython() (string, string, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := "no Python 3.10 or newer was found on this PC.\n\n" +
|
// The advice has to match the machine. Telling a Mac user to tick "Add
|
||||||
" Install it from https://www.python.org/downloads/windows/\n" +
|
// 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" +
|
" and tick \"Add python.exe to PATH\" on the first screen,\n" +
|
||||||
" then run this again."
|
" 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 {
|
if len(tried) > 0 {
|
||||||
msg += "\n\n Found, but too old: " + strings.Join(tried, ", ")
|
msg += "\n\n Found, but too old: " + strings.Join(tried, ", ")
|
||||||
}
|
}
|
||||||
|
|||||||
38
release.sh
38
release.sh
@@ -1,8 +1,9 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Build the Windows shop-PC package and publish it as a Gitea release.
|
# 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
|
# 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
|
# DEMO_PACK=demo-cameras.enc ./release.sh v0.4.4-demo
|
||||||
# a demo build: the sealed bundle from
|
# a demo build: the sealed bundle from
|
||||||
# behavision-demo-pack ships in engine-src,
|
# 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
|
REPO_API=https://gitapp.workolik.com/api/v1/repos/Loyaly/Behavision
|
||||||
STAGE=dist/Behavision
|
STAGE=dist/Behavision
|
||||||
ZIP="dist/Behavision-$TAG-windows-x64.zip"
|
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' "$*"; }
|
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
|
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}'
|
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}'
|
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; }
|
[ "${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 rev-parse -q --verify "refs/tags/$TAG" >/dev/null || git tag -a "$TAG" -m "$TAG"
|
||||||
git push -q origin "$TAG"
|
git push -q origin "$TAG"
|
||||||
# The notes come from a file so they are reviewed, not typed into a shell.
|
# 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")
|
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"])')
|
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
|
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"
|
echo " published: https://gitapp.workolik.com/Loyaly/Behavision/releases/tag/$TAG"
|
||||||
|
|||||||
Reference in New Issue
Block a user