fix: name an absolute path in the sudo re-run hint
test / unit (push) Successful in 57s
test / image-input-builds (push) Successful in 1m2s
Update Quality Badges / update-badges (push) Successful in 1m8s
test / integration-docker (push) Successful in 58s
test / coverage (push) Successful in 15s
lint / lint (push) Failing after 10m9s

The firecracker setup message told users to run

    sudo bot-bottle backend setup --backend=firecracker

which fails for exactly the users who followed the documented install. sudo
replaces PATH with sudoers' secure_path — /usr/local/sbin:/usr/local/bin:
/usr/sbin:/usr/bin:/sbin:/bin on Debian and Ubuntu — which deliberately
excludes user-writable directories. Both supported install paths land in one:
pipx uses ~/.local/bin, and install.sh's venv fallback symlinks there. So the
hint works for anyone who installed system-wide and breaks with "command not
found" for everyone else, which is how it survives a read-through.

Add bot_bottle/invocation.py: self_path() resolves the running entry point to
an absolute path, and sudo_command() builds the copy-pasteable form. The one
sudo recommendation in the tree now uses it. Non-sudo hints keep the bare
`bot-bottle`, which is correct — the user reached them by running it.

self_path() falls back to the bare name when argv[0] cannot be resolved (a
`python -m` style invocation), because a slightly wrong hint beats a traceback
raised while reporting some unrelated problem.

Tested behaviourally rather than by scanning source: the first version of the
test grepped the module and failed on the comment explaining why the bare form
is wrong. It now drives _setup_systemd() as non-root and asserts what is
actually printed. Verified the guard bites by restoring the bare form and
watching it fail.

Not verified end to end: this message only prints on the systemd path, so it
is unreachable on macOS, where the rest of this work was tested.
This commit was merged in pull request #535.
This commit is contained in:
2026-07-27 10:25:14 -04:00
parent 6385752040
commit 9bf2961d13
3 changed files with 151 additions and 2 deletions
+5 -2
View File
@@ -20,6 +20,7 @@ import subprocess
import sys
from pathlib import Path
from ... import invocation
from ... import resources
from . import netpool
from . import util
@@ -179,9 +180,11 @@ def _setup_systemd() -> None:
f"sudo systemctl daemon-reload\n"
f"sudo systemctl enable --now {netpool.SYSTEMD_UNIT}\n"
)
# Absolute path, not `sudo bot-bottle`: sudo's secure_path drops
# ~/.local/bin, where both pipx and install.sh put the entry point.
sys.stderr.write(
f"\n(Or re-run this as root to install it directly: "
f"sudo bot-bottle backend setup --backend=firecracker)\n"
f"\n(Or re-run this as root to install it directly:\n"
f" {invocation.sudo_command('backend', 'setup', '--backend=firecracker')})\n"
)
+48
View File
@@ -0,0 +1,48 @@
"""How to tell a user to re-run this CLI.
`bot-bottle …` is the right thing to print for anything the user runs as
themselves — it is on their PATH, since that is how they got here.
Under `sudo` it is not. sudo replaces PATH with sudoers' `secure_path`
(`/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin` on Debian and
Ubuntu, similar elsewhere), which deliberately excludes user-writable
directories. Both supported install paths put the entry point in one of those:
pipx uses `~/.local/bin`, and `install.sh`'s venv fallback symlinks there too.
So `sudo bot-bottle …` fails with "command not found" for exactly the users who
followed the documented install, while working for anyone who happened to
install system-wide — which is why it survives review so easily.
Naming the absolute path sidesteps secure_path entirely.
"""
from __future__ import annotations
import os
import shutil
import sys
def self_path() -> str:
"""Absolute path to this CLI's entry point.
Falls back to the bare name when the entry point cannot be resolved (an
unusual invocation such as `python -m`), because a slightly wrong hint is
better than a traceback while reporting an unrelated problem.
"""
argv0 = sys.argv[0] or "bot-bottle"
resolved = shutil.which(argv0) or argv0
if not os.path.isabs(resolved):
if os.path.exists(resolved):
resolved = os.path.abspath(resolved)
else:
return "bot-bottle"
return resolved
def sudo_command(*args: str) -> str:
"""A copy-pasteable `sudo …` invocation of this CLI.
>>> sudo_command("backend", "setup", "--backend=firecracker")
'sudo /home/u/.local/bin/bot-bottle backend setup --backend=firecracker'
"""
return " ".join(["sudo", self_path(), *args])