mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
fix(install): sign the Python that macOS grants local network access to (issue #3114)
macOS attributes Local Network permission to a code signature and judges a launchd-spawned process on its own, rather than letting it inherit the grant of the Terminal that started it. Homebrew ships Python unsigned on Intel, so there is no identity for the grant to attach to: every connection to a LAN address is dropped with no error the application can log and no permission prompt. The printer reads as unreachable and nothing says why, and the entry in Privacy & Security cannot be made to work because it refers to an identity that no longer resolves. install.sh signs during a macOS install; update_macos.sh re-checks on every update, because `brew upgrade python` installs a fresh unsigned binary under a new versioned path. Both sign only what is currently unsigned. That gate is load-bearing: on arm64 the linker ad-hoc signs every binary and the identity is a hash of the file, so re-signing would rotate it and revoke a working grant on each update. A python.org build carries a real Developer ID and must not be downgraded for the same reason. The interpreter and the framework's Python.app are both signed. The first is what sys._base_executable resolves to and what the reporter's TCC log names; the second is what his fix actually targeted. Which one macOS attributes could not be established from either, and signing both costs nothing. ----- fix(diagnostics): name the macOS permission that silently blocks the printer (issue #3114) The port checks reported all three ports unreachable while the subnet check passed, and port_mqtt's fix text sent the reporter after firewalls and IP addresses. On a macOS native install that pattern has a cause neither of those covers: no Local Network grant, denied with no error and no prompt. A new macos_local_network check, appended on macOS only so no permanently dimmed row appears for anyone else. It passes when the control port answered, which is proof the permission is in place and means the signature probe never runs on a healthy diagnostic. Otherwise it probes the interpreter: an unsigned one gets the repair that fixes it, a signed one gets System Settings — the arm64 case, where the identity is a hash of the binary, so a Python upgrade presents macOS with a new application and strands the old grant. Always warn, never fail, and only once port_mqtt has already failed, so this can never be why a green diagnostic turns red. A printer that is simply switched off produces the same all-ports-dead pattern, which is why the signature, not the pattern, is what earns the specific advice. An undeterminable signature is reported as the generic case rather than as unsigned: that advice rewrites a file in the user's Python installation and must not be offered on a guess.
This commit is contained in:
@@ -423,6 +423,80 @@ setup_virtualenv() {
|
||||
log_success "Virtual environment configured"
|
||||
}
|
||||
|
||||
# macOS attributes Local Network permission (TCC) to a process's code
|
||||
# signature, and judges a launchd-spawned process on its own rather than
|
||||
# letting it inherit the grant of the Terminal that started it. Homebrew's
|
||||
# Python is unsigned on Intel, so there is no identity for a grant to attach
|
||||
# to: every connection to a LAN address is dropped with no error the app can
|
||||
# log and no permission prompt, and the printer just reads as unreachable
|
||||
# (#3114).
|
||||
#
|
||||
# Signing only when currently unsigned is load-bearing, not tidiness. On
|
||||
# arm64 the linker ad-hoc signs every binary it produces, so the identity is
|
||||
# a hash of the file itself; re-signing rotates that hash, invalidates a
|
||||
# working grant, and causes the very outage this repairs -- on every update.
|
||||
# A python.org build carries a real Developer ID for the same reason it must
|
||||
# not be touched.
|
||||
#
|
||||
# Both the interpreter and the framework's Python.app are signed. The first
|
||||
# is what sys._base_executable resolves to (measured on an Apple Silicon
|
||||
# Homebrew install, inside and outside a venv, and named as the responsible
|
||||
# process in the reporter's own TCC log on Intel); the second is the separate
|
||||
# binary whose signature is what actually fixed his machine. Which of the two
|
||||
# macOS attributes could not be established from either, and signing both
|
||||
# costs nothing.
|
||||
sign_python_for_tcc() {
|
||||
[[ "$OS_TYPE" == "macos" ]] || return 0
|
||||
|
||||
local python_bin base_exe framework target signed_any=0
|
||||
local -a targets=()
|
||||
|
||||
python_bin="$INSTALL_PATH/venv/bin/python3"
|
||||
if [[ ! -x "$python_bin" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! command -v codesign &>/dev/null; then
|
||||
log_warn "codesign not found — skipping the macOS Local Network signing step."
|
||||
log_info "If the printer turns out to be unreachable, install the Xcode command line"
|
||||
log_info "tools with 'xcode-select --install' and re-run install/update_macos.sh."
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Checking the Python code signature (macOS Local Network permission)..."
|
||||
|
||||
base_exe="$("$python_bin" -c 'import os, sys; print(os.path.realpath(getattr(sys, "_base_executable", None) or sys.executable))' 2>/dev/null)" || return 0
|
||||
if [[ -z "$base_exe" ]] || [[ ! -e "$base_exe" ]]; then
|
||||
return 0
|
||||
fi
|
||||
targets+=("$base_exe")
|
||||
|
||||
# .../Versions/3.13/bin/python3.13 -> .../Versions/3.13/Resources/Python.app
|
||||
framework="${base_exe%/bin/*}"
|
||||
if [[ "$framework" != "$base_exe" ]] && [[ -d "$framework/Resources/Python.app" ]]; then
|
||||
targets+=("$framework/Resources/Python.app")
|
||||
fi
|
||||
|
||||
for target in "${targets[@]}"; do
|
||||
if codesign -dv "$target" &>/dev/null; then
|
||||
continue
|
||||
fi
|
||||
if codesign --force --sign - "$target" &>/dev/null; then
|
||||
log_success "Ad-hoc signed $target"
|
||||
signed_any=1
|
||||
else
|
||||
log_warn "Could not sign $target"
|
||||
log_info "Bambuddy may be unable to reach the printer. Run this by hand:"
|
||||
log_info " codesign --force --sign - \"$target\""
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$signed_any" -eq 0 ]]; then
|
||||
log_success "Python already carries a code signature"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
check_node_version() {
|
||||
# Returns 0 if Node.js 20+ is available, 1 otherwise
|
||||
if ! command -v node &>/dev/null; then
|
||||
@@ -992,6 +1066,7 @@ main() {
|
||||
|
||||
download_bambuddy
|
||||
setup_virtualenv
|
||||
sign_python_for_tcc
|
||||
build_frontend
|
||||
create_directories
|
||||
create_env_file
|
||||
|
||||
@@ -105,6 +105,64 @@ repair_loop_flag() {
|
||||
log "Without it Bambuddy runs on uvloop, which breaks RTSP cameras (#3001) and can truncate Virtual Printer FTP uploads (#1896)."
|
||||
}
|
||||
|
||||
# Re-apply the ad-hoc Python signature macOS needs to grant Local Network
|
||||
# access (#3114).
|
||||
#
|
||||
# The macOS twin of sign_python_for_tcc in install.sh, and here for two
|
||||
# reasons rather than one. An install created before that step existed has an
|
||||
# unsigned interpreter and no other way to acquire one -- the same gap
|
||||
# repair_loop_flag covers above. And it recurs: `brew upgrade python` installs
|
||||
# a fresh unsigned binary under a new versioned path, so this has to be
|
||||
# checked on every update, not once at install time.
|
||||
#
|
||||
# Without it, on an Intel Mac, TCC has no identity to anchor the grant to,
|
||||
# drops every connection to the printer with no error and no prompt, and the
|
||||
# entry in Privacy & Security cannot be made to work: the printer is simply
|
||||
# unreachable and nothing in the log says why.
|
||||
#
|
||||
# Only signs what is unsigned. On arm64 every binary already carries an
|
||||
# ad-hoc signature whose identity is a hash of the file, so re-signing would
|
||||
# rotate it and revoke a working grant on every single update.
|
||||
repair_python_signature() {
|
||||
local python_bin base_exe framework target signed_any=0
|
||||
local -a targets=()
|
||||
|
||||
python_bin="$INSTALL_DIR/venv/bin/python3"
|
||||
[ -x "$python_bin" ] || return 0
|
||||
|
||||
if ! command -v codesign >/dev/null 2>&1; then
|
||||
warn "codesign not found; skipping the macOS Local Network signing check."
|
||||
warn "If the printer is unreachable, run 'xcode-select --install' and re-run this script."
|
||||
return 0
|
||||
fi
|
||||
|
||||
base_exe="$("$python_bin" -c 'import os, sys; print(os.path.realpath(getattr(sys, "_base_executable", None) or sys.executable))' 2>/dev/null)" || return 0
|
||||
{ [ -n "$base_exe" ] && [ -e "$base_exe" ]; } || return 0
|
||||
targets+=("$base_exe")
|
||||
|
||||
# .../Versions/3.13/bin/python3.13 -> .../Versions/3.13/Resources/Python.app
|
||||
framework="${base_exe%/bin/*}"
|
||||
if [ "$framework" != "$base_exe" ] && [ -d "$framework/Resources/Python.app" ]; then
|
||||
targets+=("$framework/Resources/Python.app")
|
||||
fi
|
||||
|
||||
for target in "${targets[@]}"; do
|
||||
if codesign -dv "$target" >/dev/null 2>&1; then
|
||||
continue
|
||||
fi
|
||||
if codesign --force --sign - "$target" >/dev/null 2>&1; then
|
||||
log "Ad-hoc signed $target so macOS can grant Local Network access (#3114)"
|
||||
signed_any=1
|
||||
else
|
||||
warn "Could not sign $target; Bambuddy may be unable to reach the printer."
|
||||
warn "Run by hand: codesign --force --sign - \"$target\""
|
||||
fi
|
||||
done
|
||||
|
||||
[ "$signed_any" -eq 0 ] || log "Restart any open Bambuddy page after this update; the signature changes only take effect on the restart below."
|
||||
return 0
|
||||
}
|
||||
|
||||
on_error() {
|
||||
local exit_code="$1"
|
||||
|
||||
@@ -260,6 +318,7 @@ else
|
||||
fi
|
||||
|
||||
repair_loop_flag
|
||||
repair_python_signature
|
||||
|
||||
log "Starting service: $SERVICE_NAME"
|
||||
launchctl load "$PLIST_PATH"
|
||||
|
||||
Reference in New Issue
Block a user