fix(install): make macOS native install rootless (brew + venv permission errors)

macOS mixed root-only steps (default /opt path, sudo git clone) with steps
    that must not run as root: brew refuses to run as root, and a root-owned
    venv/node_modules can't be managed by the launchd agent. The installer now
    refuses sudo on macOS, defaults to ~/bambuddy, and drops sudo from the
    download/venv/frontend/env/dir steps. A --path under a root-owned parent still
    works via a single elevate-and-chown. Linux (service user + systemd) unchanged.
This commit is contained in:
maziggy
2026-07-07 11:05:17 +02:00
+71 -7
View File
@@ -332,6 +332,26 @@ create_user() {
log_success "Service user created"
}
# Ensure a directory exists and is owned by the current user. Used on macOS so
# the install tree stays user-owned (git/venv/npm never touch a root-owned dir).
# Only elevates when the target's parent is root-owned (e.g. /opt); a path under
# $HOME is created without any sudo prompt.
ensure_user_owned_dir() {
local dir="$1"
if [[ -d "$dir" ]] && [[ -w "$dir" ]]; then
return
fi
if mkdir -p "$dir" 2>/dev/null; then
return
fi
log_info "Creating $dir (requires your password)..."
sudo mkdir -p "$dir"
sudo chown "$(id -un):$(id -gn)" "$dir"
}
download_bambuddy() {
log_info "Downloading BamBuddy..."
@@ -343,7 +363,23 @@ download_bambuddy() {
exit 1
fi
if [[ -d "$INSTALL_PATH/.git" ]]; then
if [[ "$OS_TYPE" == "macos" ]]; then
# macOS has no service user — the whole install runs as the current user.
# Create the target user-owned (elevating only if its parent is root-owned,
# e.g. /opt), then clone/update without sudo so the venv/frontend the user
# builds next aren't fighting a root-owned tree.
ensure_user_owned_dir "$INSTALL_PATH"
if [[ -d "$INSTALL_PATH/.git" ]]; then
log_info "Existing installation found, updating..."
git config --global --add safe.directory "$INSTALL_PATH" 2>/dev/null || true
cd "$INSTALL_PATH"
git fetch origin
git checkout "$BRANCH" 2>/dev/null || git checkout -b "$BRANCH" "origin/$BRANCH"
git reset --hard "origin/$BRANCH"
else
git clone --branch "$BRANCH" https://github.com/maziggy/bambuddy.git "$INSTALL_PATH"
fi
elif [[ -d "$INSTALL_PATH/.git" ]]; then
log_info "Existing installation found, updating..."
# Add safe.directory to avoid "dubious ownership" error when running as root
git config --global --add safe.directory "$INSTALL_PATH" 2>/dev/null || true
@@ -472,9 +508,12 @@ build_frontend() {
create_directories() {
log_info "Creating data directories..."
sudo mkdir -p "$DATA_DIR" "$LOG_DIR"
if [[ "$OS_TYPE" != "macos" ]]; then
if [[ "$OS_TYPE" == "macos" ]]; then
# Rootless: DATA_DIR/LOG_DIR default under the user-owned install path.
ensure_user_owned_dir "$DATA_DIR"
ensure_user_owned_dir "$LOG_DIR"
else
sudo mkdir -p "$DATA_DIR" "$LOG_DIR"
sudo chown -R "$SERVICE_USER:$SERVICE_USER" "$DATA_DIR" "$LOG_DIR"
fi
@@ -503,11 +542,15 @@ LOG_LEVEL=$LOG_LEVEL
LOG_TO_FILE=true
EOF
sudo mv /tmp/bambuddy.env "$env_file"
if [[ "$OS_TYPE" != "macos" ]]; then
if [[ "$OS_TYPE" == "macos" ]]; then
# Rootless: install path is user-owned, so write it directly.
mv /tmp/bambuddy.env "$env_file"
chmod 600 "$env_file"
else
sudo mv /tmp/bambuddy.env "$env_file"
sudo chown "$SERVICE_USER:$SERVICE_USER" "$env_file"
sudo chmod 600 "$env_file"
fi
sudo chmod 600 "$env_file"
log_success "Environment file created at $env_file"
}
@@ -845,6 +888,27 @@ main() {
detect_os
log_success "Detected: $OS_TYPE (package manager: $PKG_MANAGER)"
# macOS must run rootless. Homebrew hard-refuses to run as root, and a venv
# / node_modules created by root can't be managed by the launchd agent (which
# runs as the user). Bail early with an actionable message instead of dying
# halfway through on "brew: running as root is not supported".
if [[ "$OS_TYPE" == "macos" ]]; then
if [[ "$EUID" -eq 0 ]]; then
log_error "Don't run the macOS installer with sudo."
log_info "Homebrew, the Python venv, and the launchd agent must all be created as your"
log_info "normal user. Re-run without sudo (the script elevates only when it truly needs to):"
echo ""
echo " ./install.sh"
echo ""
exit 1
fi
# /opt requires sudo to create and would leave a root-owned tree; default
# macOS installs to a user-owned location so the whole flow stays rootless.
if [[ "$DEFAULT_INSTALL_PATH" == "/opt/bambuddy" ]]; then
DEFAULT_INSTALL_PATH="$HOME/bambuddy"
fi
fi
# Check/install Python
if ! detect_python; then
log_info "Python 3.10+ not found, will install..."