-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathbuild.sh
More file actions
executable file
·231 lines (200 loc) · 8.58 KB
/
Copy pathbuild.sh
File metadata and controls
executable file
·231 lines (200 loc) · 8.58 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
#!/usr/bin/env bash
# Builds StackNudge.app (single persistent binary: panel + banners + voice)
# Usage: ./build.sh [arm64|x86_64] (defaults to host arch)
set -e
ARCH="${1:-$(uname -m)}"
APP="build/StackNudge.app"
build_app() {
local app="$1"
local binary_name="$2"
local plist_path="$3"
local icon_path="$4"
local target="$5"
local contents="$app/Contents"
local macos="$contents/MacOS"
mkdir -p "$macos" "$contents/Resources"
shift 5
swiftc "$@" \
-o "$macos/$binary_name" \
-target "${ARCH}-apple-macos${target}"
cp "$plist_path" "$contents/Info.plist"
if [[ -f "$icon_path" ]]; then
cp "$icon_path" "$contents/Resources/Icon.icns"
fi
# Bundle the user-facing runtime payload (hook script, phrase pools,
# example config) into the .app so Bootstrap.swift can copy them out
# to ~/.stack-nudge/ on first launch. Previously these lived only at
# the repo root and install.sh copied them; now the .app is self-
# contained — drop in Applications/, no source clone needed.
local repo_root
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cp "$repo_root/notify.sh" "$contents/Resources/notify.sh"
chmod +x "$contents/Resources/notify.sh"
if [[ -d "$repo_root/phrases" ]]; then
cp -R "$repo_root/phrases" "$contents/Resources/phrases"
fi
if [[ -f "$repo_root/notify.conf.example" ]]; then
cp "$repo_root/notify.conf.example" "$contents/Resources/notify.conf.example"
fi
# Optional: bundle a self-contained Python + stackvox into Resources/venv/.
# Skipped for local iteration (slow); enabled by CI for release artifacts.
# Opt-in via STACKNUDGE_BUNDLE_VENV=1. Bootstrap.swift gracefully handles
# the missing-venv case (skips daemon-plist registration, voice
# notifications become unavailable).
if [[ "${STACKNUDGE_BUNDLE_VENV:-0}" == "1" ]]; then
bundle_venv "$contents/Resources/venv" "$ARCH"
else
echo " (skipped venv bundle — STACKNUDGE_BUNDLE_VENV=1 to include voice engine)"
fi
sign_bundle "$app"
}
# Download a portable Python from python-build-standalone, untar it into
# $venv_dir, and pip-install stackvox into its site-packages. Result is a
# self-contained Python install that the .app can ship as Resources/venv/.
#
# Pinned PBS release; bump when stackvox's Python requirement changes or
# Apple ships a Python.framework update that breaks the current bundle.
PBS_RELEASE="20250712"
PBS_PYTHON_VERSION="3.12.11"
bundle_venv() {
local venv_dir="$1"
local arch="$2"
echo "Bundling stackvox venv ($arch, python ${PBS_PYTHON_VERSION})..."
local pbs_arch
case "$arch" in
arm64) pbs_arch="aarch64-apple-darwin" ;;
x86_64) pbs_arch="x86_64-apple-darwin" ;;
*)
echo " ! unknown arch '$arch' — skipping venv bundle"
return 0
;;
esac
local url="https://github.com/indygreg/python-build-standalone/releases/download/${PBS_RELEASE}/cpython-${PBS_PYTHON_VERSION}+${PBS_RELEASE}-${pbs_arch}-install_only.tar.gz"
local cache="/tmp/stack-nudge-pbs-${pbs_arch}.tar.gz"
if [[ ! -f "$cache" ]]; then
echo " Downloading $url"
curl -fsSL --retry 3 -o "$cache" "$url"
else
echo " Using cached $cache"
fi
rm -rf "$venv_dir"
mkdir -p "$venv_dir"
# PBS tarballs unpack into a single `python/` directory at the top —
# strip it so our $venv_dir layout matches a normal Python prefix.
tar -xzf "$cache" -C "$venv_dir" --strip-components=1
# pip install stackvox into the bundled Python's site-packages directly
# (no nested virtualenv layer — keeps the bundle a few MB smaller and
# avoids a redundant Python symlink dance).
echo " Installing stackvox..."
"$venv_dir/bin/python3" -m pip install --no-cache-dir --quiet 'stackvox>=0.4.0'
# Strip __pycache__ and pip caches to shrink the bundle. These can be
# regenerated by the bundled Python at first import — small startup
# cost, meaningful disk save (5-10% of bundle).
find "$venv_dir" -name '__pycache__' -prune -exec rm -rf {} + 2>/dev/null || true
echo " Bundled venv at $venv_dir"
}
# Sign the bundle so Info.plist is bound into the signature. Without this,
# macOS records the wrong identity for TCC (AXIsProcessTrusted = false).
#
# Resolution order:
# 1. $STACKNUDGE_SIGN_IDENTITY (explicit override — used by CI when a
# release artifact needs to be signed with a known identity from a
# secret-loaded keychain).
# 2. First "Developer ID Application" identity in the user's keychain
# (devs with the cert get stable code-sig hashes across rebuilds, so
# macOS TCC/Keychain grants stick).
# 3. Ad-hoc (`codesign -s -`) — old behaviour. Works for everyone but the
# cdhash changes on every build, which means TCC + Keychain prompts
# re-fire on each rebuild and each release.
#
# When a Developer ID is in play AND Resources/venv/ exists (CI release path),
# we recursively sign every binary inside the venv first (libs/exes/.so),
# applying hardened-runtime entitlements that the bundled Python interpreter
# needs to function. The outer .app is signed last so its signature includes
# the freshly-signed inner content.
sign_bundle() {
local app="$1"
local identity="${STACKNUDGE_SIGN_IDENTITY:-}"
if [[ -z "$identity" ]]; then
identity=$(
security find-identity -v -p codesigning 2>/dev/null \
| awk -F'"' '/"Developer ID Application/ {print $2; exit}'
)
fi
if [[ -n "$identity" ]]; then
local entitlements
entitlements="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/panel/entitlements.plist"
# Recursively sign bundled venv contents first when present.
if [[ -d "$app/Contents/Resources/venv" ]]; then
sign_venv_contents "$app/Contents/Resources/venv" "$identity" "$entitlements"
fi
# Outer .app signed last; its sig pins the inner content via cdhash.
codesign --force --options runtime --sign "$identity" \
--entitlements "$entitlements" "$app"
echo " Signed: $identity"
else
# Ad-hoc fallback. --deep recursively signs any nested code but
# without hardened runtime or entitlements (notarisation needs Dev
# ID anyway, so they'd be inert here). Bundle is fine for local
# iteration; doesn't notarise.
codesign --force --deep --sign - "$app"
echo " Signed: ad-hoc (no Developer ID Application cert in keychain)"
fi
}
# Recursively sign every native binary inside the bundled venv. Apple
# notarization requires every Mach-O inside the .app to be signed with
# our Developer ID + hardened runtime + the same entitlements.
sign_venv_contents() {
local venv="$1"
local identity="$2"
local entitlements="$3"
echo " Signing venv contents..."
# Find every Mach-O candidate: .dylib, .so, the bin/* executables, and
# the python framework's nested binaries. -print0 / xargs -0 handles
# spaces in paths (uncommon but defensive).
#
# Counter-intuitively we do NOT need to sign in depth-first order; we
# do need to sign EACH binary at least once before the outer .app is
# signed (which happens after this function returns). codesign --force
# makes the re-sign idempotent.
local signed=0
while IFS= read -r -d '' file; do
codesign --force --options runtime --sign "$identity" \
--entitlements "$entitlements" "$file" 2>/dev/null || true
signed=$((signed + 1))
done < <(
find "$venv" \
\( -name '*.dylib' -o -name '*.so' \) \
-print0
)
# Sign exec bits in bin/ (typically python3, pip, stackvox).
if [[ -d "$venv/bin" ]]; then
while IFS= read -r -d '' file; do
# Skip shebang scripts — they're not Mach-O; codesign would fail.
if file "$file" 2>/dev/null | grep -q 'Mach-O'; then
codesign --force --options runtime --sign "$identity" \
--entitlements "$entitlements" "$file" 2>/dev/null || true
signed=$((signed + 1))
fi
done < <(find "$venv/bin" -type f -perm -u+x -print0)
fi
echo " Signed $signed venv binaries"
}
echo "Building stack-nudge ($ARCH)..."
rm -rf build
mkdir -p build
# Exclude the dev build from Spotlight so re-builds don't surface a stale
# StackNudge.app duplicate next to ~/Applications/StackNudge.app in search
# results. Touched here (not committed) because `rm -rf build` wipes it
# every run.
touch build/.metadata_never_index
# Sources are globbed (mirrors Package.swift's `sources: ["panel","shared"]`)
# so new files are picked up automatically — no hand-maintained list to drift.
build_app "$APP" "stack-nudge" \
"panel/Info.plist" "notifier/Icon.icns" "13.0" \
panel/*.swift \
shared/*.swift \
-framework Foundation -framework AppKit -framework SwiftUI -framework Carbon \
-framework UserNotifications
echo " Built $APP"
echo " Binary: $(file "$APP/Contents/MacOS/stack-nudge")"