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:
maziggy
2026-09-19 16:03:52 +02:00
parent 70b42d1c8d
commit 1ccaf74dd5
23 changed files with 505 additions and 17 deletions
+75
View File
@@ -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
+59
View 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"