Repository navigation
Expand file tree
/
Copy pathinstall.sh
More file actions
executable file
·274 lines (231 loc) · 8.56 KB
/
Copy pathinstall.sh
File metadata and controls
executable file
·274 lines (231 loc) · 8.56 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
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
#!/bin/sh
set -euo pipefail
# Sabbel installer
# Usage: curl -fsSL https://raw.githubusercontent.com/kenodressel/sabbel/main/install.sh | sh
# A specific release (e.g. a prerelease): ... | SABBEL_VERSION=v1.0.0-rc.1 sh
REPO="kenodressel/sabbel"
APP_NAME="Sabbel.app"
ZIP_NAME="Sabbel.zip"
INSTALL_DIR="${HOME}/Applications"
if [ -n "${SABBEL_VERSION:-}" ]; then
DOWNLOAD_URL="https://github.com/${REPO}/releases/download/${SABBEL_VERSION}/${ZIP_NAME}"
else
DOWNLOAD_URL="https://github.com/${REPO}/releases/latest/download/${ZIP_NAME}"
fi
PLIST_LABEL="com.sabbel.agent"
PLIST_DIR="${HOME}/Library/LaunchAgents"
PLIST_PATH="${PLIST_DIR}/${PLIST_LABEL}.plist"
BUNDLE_ID="com.sabbel.app"
# ---------------------------------------------------------------------------
# Permissions (TCC) across an update
#
# macOS ties the Accessibility and Microphone grants to the app's designated
# requirement. A release signed by the stable "Sabbel Release" certificate has
# the same one as the release before it, so its grants carry over and must be
# kept. When the installed app is ad hoc signed (every build is a new
# identity, e.g. a 0.5.x Python build) or its requirement differs from the new
# app's, the old grant no longer matches: macOS shows the switch as on while
# denying the app. Only then are the grants reset, so macOS asks again.
#
# Shared with `make install-app`, which sources this file with
# SABBEL_INSTALL_LIB=1 (as does scripts/test_install_tcc.sh).
# ---------------------------------------------------------------------------
# Exit 0 when the grants must be reset, 1 when they can be kept. Decided on
# the designated requirement alone (`codesign -d -r-`): it is what TCC
# matches, and an ad-hoc app's is always a `cdhash` (new with every build).
# Reset when the installed app's requirement is missing (unsigned or
# unreadable), is a cdhash, or differs from the new app's.
# $1 `codesign -d -r-` output of the installed app
# $2 `codesign -d -r-` output of the new app
sabbel_needs_tcc_reset() {
# Compare only the requirement: the Executable= line names each app's path.
# Ad-hoc output prints it as a comment ("# designated => cdhash ...").
_sabbel_old_dr="$(printf '%s\n' "$1" | sed -n 's/^#* *designated => //p')"
_sabbel_new_dr="$(printf '%s\n' "$2" | sed -n 's/^#* *designated => //p')"
case "${_sabbel_old_dr}" in
''|cdhash*) return 0 ;;
esac
if [ "${_sabbel_old_dr}" = "${_sabbel_new_dr}" ]; then
return 1
fi
return 0
}
# Exit 0 when replacing the installed app $1 with the new app $2 needs a reset.
sabbel_tcc_reset_needed() {
sabbel_needs_tcc_reset \
"$(codesign -d -r- "$1" 2>&1 || true)" \
"$(codesign -d -r- "$2" 2>&1 || true)"
}
sabbel_reset_tcc() {
tccutil reset Accessibility "${BUNDLE_ID}" >/dev/null 2>&1 || true
tccutil reset Microphone "${BUNDLE_ID}" >/dev/null 2>&1 || true
}
# Sourced for the functions above only: stop before installing anything.
if [ "${SABBEL_INSTALL_LIB:-}" = 1 ]; then return 0; fi
# ---------------------------------------------------------------------------
# Output helpers
# ---------------------------------------------------------------------------
if [ -t 1 ]; then
BOLD="$(printf '\033[1m')"
RED="$(printf '\033[31m')"
GREEN="$(printf '\033[32m')"
YELLOW="$(printf '\033[33m')"
CYAN="$(printf '\033[36m')"
RESET="$(printf '\033[0m')"
else
BOLD=""
RED=""
GREEN=""
YELLOW=""
CYAN=""
RESET=""
fi
info() {
printf "%s==>%s %s%s\n" "${CYAN}${BOLD}" "${RESET}" "$*" "${RESET}"
}
success() {
printf "%s==>%s %s%s\n" "${GREEN}${BOLD}" "${RESET}" "$*" "${RESET}"
}
warn() {
printf "%sWarning:%s %s%s\n" "${YELLOW}${BOLD}" "${RESET}" "$*" "${RESET}" >&2
}
error() {
printf "%sError:%s %s%s\n" "${RED}${BOLD}" "${RESET}" "$*" "${RESET}" >&2
exit 1
}
# ---------------------------------------------------------------------------
# System checks
# ---------------------------------------------------------------------------
OS="$(uname -s)"
ARCH="$(uname -m)"
if [ "${OS}" != "Darwin" ]; then
error "Sabbel requires macOS. Detected OS: ${OS}"
fi
if [ "${ARCH}" != "arm64" ]; then
error "Sabbel requires Apple Silicon (arm64). Detected architecture: ${ARCH}"
fi
# ---------------------------------------------------------------------------
# Temp directory with automatic cleanup
# ---------------------------------------------------------------------------
TMPDIR="$(mktemp -d)"
trap 'rm -rf "${TMPDIR}"' EXIT
# ---------------------------------------------------------------------------
# Download
# ---------------------------------------------------------------------------
info "Downloading Sabbel ${SABBEL_VERSION:-(latest release)}..."
if command -v curl > /dev/null 2>&1; then
curl --fail --silent --show-error --location --output "${TMPDIR}/${ZIP_NAME}" "${DOWNLOAD_URL}"
else
error "curl is required but was not found."
fi
# ---------------------------------------------------------------------------
# Extract
# ---------------------------------------------------------------------------
info "Extracting ${ZIP_NAME}..."
ditto -xk "${TMPDIR}/${ZIP_NAME}" "${TMPDIR}/"
if [ ! -d "${TMPDIR}/${APP_NAME}" ]; then
error "Expected ${APP_NAME} inside ${ZIP_NAME}, but it was not found."
fi
# ---------------------------------------------------------------------------
# Install
# ---------------------------------------------------------------------------
mkdir -p "${INSTALL_DIR}"
DEST="${INSTALL_DIR}/${APP_NAME}"
IS_UPDATE=false
TCC_RESET=false
if [ -d "${DEST}" ]; then
IS_UPDATE=true
info "Updating existing installation..."
# Stop running Sabbel
pkill -x "Sabbel" 2>/dev/null || true
DOMAIN="gui/$(id -u)"
launchctl bootout "${DOMAIN}/${PLIST_LABEL}" 2>/dev/null || true
sleep 1
if sabbel_tcc_reset_needed "${DEST}" "${TMPDIR}/${APP_NAME}"; then
info "The app's signature changed; resetting its Accessibility and Microphone permissions..."
sabbel_reset_tcc
TCC_RESET=true
fi
rm -rf "${DEST}"
fi
info "Installing to ${DEST}..."
ditto "${TMPDIR}/${APP_NAME}" "${DEST}"
# ---------------------------------------------------------------------------
# Launch (macOS focuses it if it's already running via autostart below)
# ---------------------------------------------------------------------------
open "${DEST}"
# ---------------------------------------------------------------------------
# Autostart prompt
# ---------------------------------------------------------------------------
setup_autostart() {
mkdir -p "${PLIST_DIR}"
cat > "${PLIST_PATH}" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>${PLIST_LABEL}</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/open</string>
<string>-a</string>
<string>${DEST}</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StandardOutPath</key>
<string>/tmp/sabbel.log</string>
<key>StandardErrorPath</key>
<string>/tmp/sabbel.log</string>
</dict>
</plist>
PLIST
DOMAIN="gui/$(id -u)"
launchctl bootout "${DOMAIN}/${PLIST_LABEL}" 2>/dev/null || true
launchctl bootstrap "${DOMAIN}" "${PLIST_PATH}"
success "Sabbel will start automatically on login."
}
# Parse flags
AUTOSTART=true
for arg in "$@"; do
case "${arg}" in
--no-autostart) AUTOSTART=false ;;
esac
done
if [ -t 0 ]; then
# Interactive — ask the user
printf "\n"
printf "%sStart Sabbel on login?%s [Y/n] " "${BOLD}" "${RESET}"
read -r REPLY
case "${REPLY}" in
[nN]*) AUTOSTART=false ;;
esac
fi
if [ "${AUTOSTART}" = true ]; then
setup_autostart
fi
# ---------------------------------------------------------------------------
# Success
# ---------------------------------------------------------------------------
printf "\n"
if [ "${IS_UPDATE}" = true ]; then
success "Sabbel has been updated and launched."
printf "\n"
if [ "${TCC_RESET}" = true ]; then
printf " The app's signature changed (expected once, when updating from a\n"
printf " 0.5.x build), so its Accessibility and Microphone permissions were\n"
printf " reset. Grant them again when macOS asks; the Sabbel menu shows what\n"
printf " is still missing. Later updates keep your permissions.\n"
else
printf " Same signature as before: your Accessibility and Microphone\n"
printf " permissions were kept.\n"
fi
else
success "Sabbel has been installed to ${BOLD}${DEST}${RESET} and launched."
printf "\n"
printf " Sabbel lives in the menu bar. Open its menu to see its status and\n"
printf " which permissions it still needs.\n"
fi
printf "\n"